Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] Dash 1.5.0 inclusion of dash.DataTable and dbc.Modal breaks all callbacks #277

Closed
christianwengert opened this issue Nov 4, 2019 · 10 comments

Comments

@christianwengert
Copy link

Describe your context

We have a fairly complex app which was wonderfully working on Dash 1.4.0 and before. Now I updated to Dash 1.5.0 and subsequently to 1.5.1 and the fact of including a DataTable makes all callbacks not working anymore (even the clientside ones).

I managed to break it down to a minimal working example which exposes the error:

This does work (i.e. clicking on one of the "Settings" Buttons make a modal appear:

import dash
import dash_html_components as html
from dash import no_update
import dash_table as dt
from dash.dependencies import Input, Output
import dash_bootstrap_components as dbc

NODE_SETTINGS_MODAL_PREFIX = 'test'
FIELD_SETTINGS = 'field-settings'
FS_OPTIONS = 'options'


app = dash.Dash(__name__)


def layout() -> html.Div:
    div1 = html.Div(children=[
        html.Span('Node 1', id=f'label-{1}'),
        html.Button('Settings', id=f'settings-{1}'),

    ])

    div2 = html.Div(children=[
        html.Span('Node 2', id=f'label-{2}'),
        html.Button('Settings', id=f'settings-{2}'),

    ])

    layout = html.Div([
        build_node_settings_modal(),
        div1,
        div2
    ])
    return layout


def build_node_settings_modal() -> dbc.Modal:

    return dbc.Modal(
        [
            dbc.ModalHeader(html.Div([
                "Settings",
                dbc.Button(id=f"{NODE_SETTINGS_MODAL_PREFIX}-close", className="modal-close-button fas fa-times")
            ])),
            dbc.ModalBody([
                html.Div(id=f'{NODE_SETTINGS_MODAL_PREFIX}-settings', children=[
                    # dt.DataTable(
                    #     id=f'{FIELD_SETTINGS}-{FS_OPTIONS}',
                    #     columns=[{"name": i, "id": i} for i in ['value', 'label']],
                    #     data=[{'value': 'Value 1', 'label': 'Item 1'}]
                    # ),
                ])
            ]),
        ], id=NODE_SETTINGS_MODAL_PREFIX,
    )


app.layout = layout


@app.callback(
    [Output(NODE_SETTINGS_MODAL_PREFIX, "is_open"),
     Output(NODE_SETTINGS_MODAL_PREFIX, "className")],  # not a genius thing to abuse the className
    [Input(f"{NODE_SETTINGS_MODAL_PREFIX}-close", "n_clicks"),
     *[Input(f'settings-{i}', 'n_clicks') for i in [1, 2]],
     ]
)
def toggle_node_settings_modal(*args):
    if all(a is None for a in args):
        return no_update, no_update

    ctx = dash.callback_context
    triggered = ctx.triggered[0]

    if triggered['prop_id'] == f'{NODE_SETTINGS_MODAL_PREFIX}-close.n_clicks':  # Force close on Escape, close button and click beside the modal
        return False, ""

    node_id = triggered['prop_id'].split('.')[0].split('-')[1]

    return True, node_id


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


When I uncomment the dash_table part:

import dash
import dash_html_components as html
from dash import no_update
import dash_table as dt
from dash.dependencies import Input, Output
import dash_bootstrap_components as dbc

NODE_SETTINGS_MODAL_PREFIX = 'test'
FIELD_SETTINGS = 'field-settings'
FS_OPTIONS = 'options'


app = dash.Dash(__name__)


def layout() -> html.Div:
    div1 = html.Div(children=[
        html.Span('Node 1', id=f'label-{1}'),
        html.Button('Settings', id=f'settings-{1}'),

    ])

    div2 = html.Div(children=[
        html.Span('Node 2', id=f'label-{2}'),
        html.Button('Settings', id=f'settings-{2}'),

    ])

    layout = html.Div([
        build_node_settings_modal(),
        div1,
        div2
    ])
    return layout


def build_node_settings_modal() -> dbc.Modal:

    return dbc.Modal(
        [
            dbc.ModalHeader(html.Div([
                "Settings",
                dbc.Button(id=f"{NODE_SETTINGS_MODAL_PREFIX}-close", className="modal-close-button fas fa-times")
            ])),
            dbc.ModalBody([
                html.Div(id=f'{NODE_SETTINGS_MODAL_PREFIX}-settings', children=[
                    dt.DataTable(
                        id=f'{FIELD_SETTINGS}-{FS_OPTIONS}',
                        columns=[{"name": i, "id": i} for i in ['value', 'label']],
                        data=[{'value': 'Value 1', 'label': 'Item 1'}]
                    ),
                ])
            ]),
        ], id=NODE_SETTINGS_MODAL_PREFIX,
    )


app.layout = layout


@app.callback(
    [Output(NODE_SETTINGS_MODAL_PREFIX, "is_open"),
     Output(NODE_SETTINGS_MODAL_PREFIX, "className")],  # not a genius thing to abuse the className
    [Input(f"{NODE_SETTINGS_MODAL_PREFIX}-close", "n_clicks"),
     *[Input(f'settings-{i}', 'n_clicks') for i in [1, 2]],
     ]
)
def toggle_node_settings_modal(*args):
    if all(a is None for a in args):
        return no_update, no_update

    ctx = dash.callback_context
    triggered = ctx.triggered[0]

    if triggered['prop_id'] == f'{NODE_SETTINGS_MODAL_PREFIX}-close.n_clicks':  # Force close on Escape, close button and click beside the modal
        return False, ""

    node_id = triggered['prop_id'].split('.')[0].split('-')[1]

    return True, node_id


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

The callback won't fire anymore (and there is not even a callback related to the table!)
So no modal showing up

  • replace the result of pip list | grep dash below
dash                      1.5.1  
dash-bootstrap-components 0.7.2  
dash-canvas               0.0.11 
dash-core-components      1.4.0  
dash-cytoscape            0.1.1  
dash-html-components      1.0.1  
dash-renderer             1.2.0  
dash-table                4.5.0  

  • if frontend related, tell us your Browser, Version and OS

    • OS: OSX
    • Browser Chrome, Vivaldi, Safari

Describe the bug

Having a DataTable in the layout breaks all callbacks, i.e callbacks are not firing at all anymore
Uncommenting the use of DataTable makes everything OK

Expected behavior

Callback working

@christianwengert
Copy link
Author

christianwengert commented Nov 4, 2019

I found the following: When replacing the dash_bootstrap_components.Modal.is_open property with a simple dash_html_components.div.style property:

"""

"""
import dash
import dash_html_components as html
from dash import no_update
import dash_table as dt
from dash.dependencies import Input, Output
import dash_bootstrap_components as dbc

NODE_SETTINGS_MODAL_PREFIX = 'test'
FIELD_SETTINGS = 'field-settings'
FS_OPTIONS = 'options'


app = dash.Dash(__name__)


def layout() -> html.Div:
    div1 = html.Div(children=[
        html.Span('Node 1', id=f'label-{1}'),
        html.Button('Settings', id=f'settings-{1}'),

    ])

    div2 = html.Div(children=[
        html.Span('Node 2', id=f'label-{2}'),
        html.Button('Settings', id=f'settings-{2}'),

    ])

    layout = html.Div([
        build_node_settings_modal(),
        div1,
        div2
    ])
    return layout


def build_node_settings_modal() -> dbc.Modal:
    return html.Div(
        [
            html.Div([
                html.Div(id=f'{NODE_SETTINGS_MODAL_PREFIX}-settings', children=[
                    dt.DataTable(
                        id=f'{FIELD_SETTINGS}-{FS_OPTIONS}',
                        columns=[{"name": i, "id": i} for i in ['value', 'label']],
                        data=[{'value': 'Value 1', 'label': 'Item 1'}]
                    ),
                ])
            ]),
        ], id=NODE_SETTINGS_MODAL_PREFIX,
        style={'display': 'none'}
    )


app.layout = layout


@app.callback(
    [Output(NODE_SETTINGS_MODAL_PREFIX, "style"),
     Output(NODE_SETTINGS_MODAL_PREFIX, "className")],  # not a genius thing to abuse the className
    [
     *[Input(f'settings-{i}', 'n_clicks') for i in [1, 2]],
     ]
)
def toggle_node_settings_modal(*args):
    if all(a is None for a in args):
        return no_update, no_update

    ctx = dash.callback_context
    triggered = ctx.triggered[0]

    if triggered['prop_id'] == f'{NODE_SETTINGS_MODAL_PREFIX}-close.n_clicks':  # Force close on Escape, close button and click beside the modal
        return {'display': 'none'}, ""

    node_id = triggered['prop_id'].split('.')[0].split('-')[1]

    return {'display': 'block'}, node_id


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

Everything works as expected => So no bug in dash, but in dash bootstrap components.
See also plotly/dash#998 (I closed it over there)

@christianwengert
Copy link
Author

probably related to plotly/dash#995

@christianwengert
Copy link
Author

Still the same problem with Dash 1.6.0

@tcbegley
Copy link
Collaborator

tcbegley commented Nov 5, 2019

Hey @christianwengert

Thanks for reporting this and for doing so in such detail.

I don't fully understand the issue yet, but my best guess is that it's a combination of the lazy loading of JavaScript bundles added in Dash 1.5.0 (see here and here) and the fact that the modal contents are not in the page layout on load, toggling the is_open prop causes new elements to get rendered. This is a little different to your second example where the DataTable starts off in the page layout, but is hidden by CSS.

I'm going to need a bit more time to properly understand the issue and figure out how / whether it can be fixed on the dash-bootstrap-components side, but for now here's a temporary workaround. Simply add a hidden empty DataTable to the layout to force the everything to be loaded.

def layout() -> html.Div:
    ...

    hidden_table = html.Div(dt.DataTable(), style={"display": "none"})

    layout = html.Div([build_node_settings_modal(), div1, div2, hidden_table])
    return layout

Testing that on my end it resolves things for now, even if it's slightly unsatisfactory. Let me know how you get on, and I'll look into a better fix.

@christianwengert
Copy link
Author

Indeed, this workaround also works in my large app. I would not understand why or how, but thanks for pointing this out!

@josephcappadona
Copy link

josephcappadona commented Nov 21, 2019

@tcbegley I just wanted to point out that I am encountering this bug without any use of the dbc module in my code, my code only contains a dt.DataTable, but the behavior is the same where callbacks are not fired until I navigate to the tab with the DataTable. And the fix you mention does not work for me.

@tcbegley
Copy link
Collaborator

Hi @josephcappadona

That's interesting, are you able to share a minimal example that reproduces the issue? If so it may be worth also raising an issue on the Plotly Dash repo.

@josephcappadona
Copy link

@tcbegley See plotly/dash#1010

@tcbegley
Copy link
Collaborator

Thanks! I'll keep an eye on that and see if there's any action needed from us.

@tcbegley
Copy link
Collaborator

This has been fixed in Dash 1.7.0. See here and here.

Feel free to reopen if you're still having trouble, but the original example at the top of this issue works after running

pip install -U dash

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants