How to make a dash bootstrap Modal pop up when clicking on a line of a datatable?

I have a datatable and I would like to create a pop-up like a dash bootstrap Modal when clicking on it:

The original code is:

import dash
import dash_table
import os
import pandas as pd

PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
DATA_PATH = os.path.join(PROJECT_ROOT, '../data/')
df = pd.read_csv(DATA_PATH + 'tickers_september_2017_red.csv')

app = dash.Dash(__name__)


def layout():
    return dash_table.DataTable(
        id='table',
        columns=[{"name": i, "id": i} for i in df.columns],
        data=df.to_dict('records'),
    )


if __name__ == '__main__':
    app.run_server(debug=False, port=8051)

My attempt is:

import dash
import dash_table
import os
import pandas as pd
from dash.dependencies import Input, Output
import dash_html_components as html
import dash_bootstrap_components as dbc



PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
DATA_PATH = os.path.join(PROJECT_ROOT, '../data/')
df = pd.read_csv(DATA_PATH + 'tickers_september_2017_red.csv')

app = dash.Dash(__name__)

@app.callback(
    Output('table', 'children'),
    [Input('table', 'active_cell')])
def update_graphs(active_cell):
    active_row_id = active_cell['row_id'] if active_cell else None

    modal = html.Div(
        [
            dbc.Button("Open modal", id="open"),
            dbc.Modal(
                [
                    dbc.ModalHeader("Header"),
                    dbc.ModalBody("This is the content of the modal"),
                    dbc.ModalFooter(
                        dbc.Button("Close", id="close", className="ml-auto")
                    ),
                ],
                id="modal",
            ),
        ]
    )
    return modal


def layout():
    return dash_table.DataTable(
        id='table',
        columns=[{"name": i, "id": i} for i in df.columns],
        data=df.to_dict('records'),
    )


if __name__ == '__main__':
    app.run_server(debug=False, port=8051)

However I get:

C:\ProgramData\Anaconda3\envs\homework3\python.exe C:/Users/antoi/Documents/Programming/portfolio-advisor/run.py
Traceback (most recent call last):
  File "C:/Users/antoi/Documents/Programming/portfolio-advisor/run.py", line 1, in <module>
    from dashboard.app import app #, auth
  File "C:\Users\antoi\Documents\Programming\portfolio-advisor\dashboard\app.py", line 8, in <module>
    from .pages import header, imap, stocks, markets, sto
  File "C:\Users\antoi\Documents\Programming\portfolio-advisor\dashboard\pages\sto.py", line 19, in <module>
    [Input('table', 'active_cell')])
  File "C:\ProgramData\Anaconda3\envs\homework3\lib\site-packages\dash\dash.py", line 1317, in callback
    self._validate_callback(output, inputs, state)
  File "C:\ProgramData\Anaconda3\envs\homework3\lib\site-packages\dash\dash.py", line 887, in _validate_callback
    """
dash.exceptions.LayoutIsNotDefined: 
Attempting to assign a callback to the application but
the `layout` property has not been assigned.
Assign the `layout` property before assigning callbacks.
Alternatively, suppress this warning by setting
`suppress_callback_exceptions=True`


Process finished with exit code 1

So I tried to assign the layout property anywhere with:

But got the following error:

C:\ProgramData\Anaconda3\envs\homework3\python.exe C:/Users/antoi/Documents/Programming/portfolio-advisor/run.py
Traceback (most recent call last):
  File "C:/Users/antoi/Documents/Programming/portfolio-advisor/run.py", line 1, in <module>
    from dashboard.app import app #, auth
  File "C:\Users\antoi\Documents\Programming\portfolio-advisor\dashboard\app.py", line 8, in <module>
    from .pages import header, imap, stocks, markets, sto
  File "C:\Users\antoi\Documents\Programming\portfolio-advisor\dashboard\pages\sto.py", line 48
    @app.layout = layout()
                ^
SyntaxError: invalid syntax

Process finished with exit code 1

I also tried to write suppress_callback_exceptions=True but that didn’t changed anything

You need to assign the layout, from your error it looked like you were trying to do this with decorator syntax @app.layout, it should just be

app.layout = layout

Once you do that it still won’t work, because you’re trying to add the modal to the children of the table. Instead you should have the Modal in the layout, and use the callback to toggle the is_open prop. Something like this

def layout():
    return html.Div([
        dash_table.DataTable(
            id='table',
            columns=[{"name": i, "id": i} for i in df.columns],
            data=df.to_dict('records'),
        ),
        dbc.Modal(
            [
                dbc.ModalHeader("Header"),
                dbc.ModalBody("This is the content of the modal"),
                dbc.ModalFooter(
                    dbc.Button("Close", id="close", className="ml-auto")
                ),
            ],
            id="modal",
        ),
    ])

app.layout = layout  # note lack of parentheses
 

@app.callback(Output('modal', 'is_open'), [Input('table', 'active_cell', Input('close', 'n_clicks')])
def update_graphs(active_cell, n):
    # your callback logic
    ...

Thanks for your answe @tcbegley .

I aready had app.layout defined in the app.py file so in this file. My bad. I just had to import it with from ..server import app.

import dash_table
import os
import pandas as pd
from dash.dependencies import Input, Output, State
import dash_html_components as html
import dash_bootstrap_components as dbc

from ..server import app


PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
DATA_PATH = os.path.join(PROJECT_ROOT, '../data/')
df = pd.read_csv(DATA_PATH + 'tickers_september_2017_red.csv')


def layout():
    return html.Div([
        dash_table.DataTable(
            id='table',
            columns=[{"name": i, "id": i} for i in df.columns],
            data=df.to_dict('records'),
        ),
        dbc.Modal(
            [
                dbc.ModalHeader("Header"),
                dbc.ModalBody("This is the content of the modal"),
                dbc.ModalFooter(
                    dbc.Button("Close", id="close", className="ml-auto")
                ),
            ],
            id="modal",
        )
    ])

#


@app.callback(Output('modal', 'is_open'),
              [Input('table', 'active_cell'),
               Input('close', 'n_clicks')],
              [State("modal", "is_open")])
def toggle_modal(n1, n2, is_open):
    if n1 or n2:
        return not is_open
    return is_open


if __name__ == '__main__':
    app.run_server(debug=False, port=8051)
    suppress_callback_exceptions = True

Yet …

  1. Now the modal shows up, but only at the bottom of the datatable, not as a pop-up as in the Modal documentation. Do you know how I can do that ?
  2. I would like the header to be the Name in the row of the cell activated. So I tried:
def layout():
    return html.Div([
        dash_table.DataTable(
            id='table',
            columns=[{"name": i, "id": i} for i in df.columns],
            data=df.to_dict('records'),
        ),
        dbc.Modal(
            [
                dbc.ModalHeader(df.iloc[active_row_id]['Name']),
                dbc.ModalBody("This is the content of the modal"),
                dbc.ModalFooter(
                    dbc.Button("Close", id="close", className="ml-auto")
                ),
            ],
            id="modal",
        )
    ])

And got a NameError: name 'active_row_id' is not defined error.

  1. You need to link a Bootstrap stylesheet. Easiest way is when you create app like this
app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
  1. active_row_id is not defined in your layout function. I think you’ll have to write a second callback that has the children of the ModalHeader as the output, and takes the active cell of the table as an input.
  1. Perfect ! It worked !

  2. Creating a second callback that has the children of the ModalHeader

Sure ! Like this ?

@app.callback(Output('ModalHeader', 'children'),
              [Input('table', 'active_cell'),
               Input('close', 'n_clicks')],
              # [State('modal', 'is_open')]
              )
def update_graph(active_cell, n_clicks):
    print("active_cell: ", active_cell)
    if active_cell is not None:
        row = df.iloc[[active_cell.get("row")]]
        return row['Name'].values[0]

But it is never fired.

You need to add an id to the ModalHeader then refer to it by id in Output when defining the callback. At the moment you just have ModalHeader, but since it’s searching by id it won’t find a component to update with the callback.

Thanks it worked ! I have now a lovely popup with it’s own Header.

Do you have any idea on how I can change the whole Modal? I tried the following:

def layout():
    return html.Div([
        dash_table.DataTable(
            id='table',
            columns=[{"name": i, "id": i} for i in df.columns],
            data=df.to_dict('records'),

            sort_action='custom',
            sort_mode='single',
            sort_by=[]
        ),
        dbc.Modal(
            [
                dbc.ModalHeader("Header", id="ModalHeader"),
                dbc.ModalBody("This is the content of the modal"),
                dbc.ModalFooter(
                    dbc.Button("Close", id="close", className="ml-auto")
                ),
            ],
            id="modal",
        )
    ])

@app.callback(Output('modal', 'children'),
              [Input('table', 'active_cell'),
              Input('close', 'n_clicks')],
              )
def update_graph(active_cell, n_clicks):
    print("active_cell: ", active_cell)
    if active_cell is not None:
        row = df.iloc[[active_cell.get("row")]]
        return dbc.Modal(
            [
                dbc.ModalHeader(row['Name'].values[0]),
                dbc.ModalBody("This will be something more interesting"),
                dbc.ModalFooter(
                    dbc.Button("Close", id="close", className="ml-auto")
                ),
            ],
            id="modal",
        )

But it doesn’t change. It’s written Updating in the window tab though.

You’re modifying the children of the modal with your callback, so you don’t want to return another modal, you’ll get nested modals that way, all with id='modal' which could be what’s causing the app to get stuck updating.

Instead, just return the children in the callback:

return [
    dbc.ModalHeader(row['Name'].values[0]),
    dbc.ModalBody("This will be something more interesting"),
    dbc.ModalFooter(
        dbc.Button("Close", id="close", className="ml-auto")
    ),
]

Sure ! So I tried:

@app.callback(Output('modal', 'children'),
              [Input('table', 'active_cell'),
              Input('close', 'n_clicks')],
              # [State('modal', 'is_open')]
              )
def update_graph(active_cell, n_clicks):
    print("active_cell: ", active_cell)
    if active_cell is not None:
        row = df.iloc[[active_cell.get("row")]]
        print("row: ", row)
        return [
            dbc.ModalHeader(row['Name'].values[0]),
            dbc.ModalBody("This will be something more interesting"),
            dbc.ModalFooter(
                dbc.Button("Close", id="close", className="ml-auto")
            ),
        ]

But nothing changed. Still stuck updating.
Yet, when I try to update the header of the modal with your code I do have this nested modals.

Any errors in the terminal or the Javascript console?

Here’s a dummy example that works for me, maybe you can test it out and compare to your version to see if you can figure out where it’s going wrong.

import dash
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State

app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])

app.layout = dbc.Container(
    [
        dbc.FormGroup(
            [dbc.Label("Custom header"), dbc.Input(id="input", value="Header")]
        ),
        dbc.Button("toggle", id="open"),
        dbc.Modal(
            [
                dbc.ModalHeader(id="modal-header"),
                dbc.ModalBody("This is the body"),
                dbc.ModalFooter(dbc.Button("close", id="close")),
            ],
            id="modal",
        ),
    ]
)


@app.callback(
    Output("modal", "is_open"),
    [Input("open", "n_clicks"), Input("close", "n_clicks")],
    [State("modal", "is_open")],
)
def toggle_modal(n1, n2, is_open):
    if n1 or n2:
        return not is_open
    return is_open


@app.callback(Output("modal", "children"), [Input("input", "value")])
def set_content(value):
    return [
        dbc.ModalHeader(value),
        dbc.ModalBody(f"Modal with header: {value}"),
        dbc.ModalFooter(dbc.Button("Close", id="close")),
    ]


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