emilhe / dash-extensions

The dash-extensions package is a collection of utility functions, syntax extensions, and Dash components that aim to improve the Dash development experience

Home Page:https://www.dash-extensions.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Combing MultiplexerTransform and blueprint embedding failed to fire callbacks

wuyuanyi135 opened this issue · comments

The following code requires dash_mantine_components and dash_iconify to execute. I used a blueprint to make a layout that add a new item when the button is clicked. However, when added to the parent app, clicking the button won't even fire a request so the callback will not be fired. If add_cb was commented out, the callback works fine.

Is there any undocumented conflict between blueprint embedding and MultiplexerTransform?

import dash_mantine_components as dmc
from dash_extensions.enrich import DashBlueprint, Input, Output, ALL, callback_context, no_update, DashProxy, html, \
    MultiplexerTransform
from dash_iconify import DashIconify

devices = []

bp = DashBlueprint(transforms=[MultiplexerTransform()])
bp.layout = dmc.Stack(
    [

        dmc.Button('Test', id='scan-device-btn', variant='outline'),
        dmc.Card(
            [
                dmc.List(
                    children=None,
                    id="scanned-device-list"
                ),
            ],
            withBorder=True
        )
    ]
)


def make_list_item(name: str, idx: int):
    return dmc.ListItem(
        [
            dmc.Group(
                [
                    dmc.Text(name, size="lg"),
                    (
                        dmc.ActionIcon(
                            DashIconify(icon="bi:plus-circle"),
                            id={"type": "add-device", "index": idx},
                            variant="subtle"
                        )
                    )
                ]
            )
        ],
        icon=DashIconify(icon="bi:wifi", height=24),
    )


def make_return_list():
    return [
        make_list_item(r, i)
        for i, r in enumerate(devices)
    ]


@bp.callback(
    Output('scanned-device-list', 'children'),
    Input({'type': 'add-device', 'index': ALL}, 'n_clicks'),
    prevent_initial_call=True,
)
def add_cb(nc1):
    idx = callback_context.triggered_id
    el = next(filter(lambda x: x == idx, callback_context.triggered_prop_ids.values()))
    if el is None:
        return no_update

    return make_return_list()


@bp.callback(
    Output('scanned-device-list', 'children'),
    Input('scan-device-btn', 'n_clicks'),
    prevent_initial_call=True,
)
def scan_cb(nc):
    if nc is None:
        return no_update

    devices.append("Test Device Name")

    return make_return_list()


app = DashProxy(__name__)
app.layout = html.Div([
    html.H2("Test Embedding"),
    bp.layout
])
bp.register_callbacks(app)

app.run_server(debug=True)

Following #243, the workaround involves one more step:
To make the blueprint work, one should either:

  1. invoke bp.register_callbacks before the line assigning app.layout or
  2. set app.layout to a function for deferred evaluation.

I guess the client-side layout was not properly generated and was cached. Also, after bp.register_callbacks the layout did not trigger a layout refresh, leading to transformed callbacks but the dcc.Store was not injected to the client-side layout.

Same question here: is this the correct way to make a reusable blueprint component?

It seems you are hitting a combination of issues here,

  1. The one you reported #243 , i.e. that you must use bp._layout_value() instead of the layout property to embed the layout
  2. Callbacks must be registered before the blueprint is embedded

As noted in #243, I believe this can be fixed by adding a syntactic alias (+ update of docs accordingly). I guess (2) could be addressed by making it clear in the docs that callback registration must happen prior to embedding. But it would of course be more elegant, if that was not needed - I'll take a look at that :)

Hence, for now, I guess the following code should work,

import dash_mantine_components as dmc
from dash_extensions.enrich import DashBlueprint, Input, Output, ALL, callback_context, no_update, DashProxy, html, \
    MultiplexerTransform
from dash_iconify import DashIconify

devices = []

bp = DashBlueprint(transforms=[MultiplexerTransform()])
bp.layout = dmc.Stack(
    [

        dmc.Button('Test', id='scan-device-btn', variant='outline'),
        dmc.Card(
            [
                dmc.List(
                    children=None,
                    id="scanned-device-list"
                ),
            ],
            withBorder=True
        )
    ]
)


def make_list_item(name: str, idx: int):
    return dmc.ListItem(
        [
            dmc.Group(
                [
                    dmc.Text(name, size="lg"),
                    (
                        dmc.ActionIcon(
                            DashIconify(icon="bi:plus-circle"),
                            id={"type": "add-device", "index": idx},
                            variant="subtle"
                        )
                    )
                ]
            )
        ],
        icon=DashIconify(icon="bi:wifi", height=24),
    )


def make_return_list():
    return [
        make_list_item(r, i)
        for i, r in enumerate(devices)
    ]


@bp.callback(
    Output('scanned-device-list', 'children'),
    Input({'type': 'add-device', 'index': ALL}, 'n_clicks'),
    prevent_initial_call=True,
)
def add_cb(nc1):
    idx = callback_context.triggered_id
    el = next(filter(lambda x: x == idx, callback_context.triggered_prop_ids.values()))
    if el is None:
        return no_update

    return make_return_list()

@bp.callback(
    Output('scanned-device-list', 'children'),
    Input('scan-device-btn', 'n_clicks'),
    prevent_initial_call=True,
)
def scan_cb(nc):
    if nc is None:
        return no_update

    devices.append("Test Device Name")

    return make_return_list()


app = DashProxy(__name__, suppress_callback_exceptions=True)
bp.register_callbacks(app)
app.layout = html.Div([
    html.H2("Test Embedding"),
    bp._layout_value()
])

app.run_server(debug=True)

Thanks for the summary @emilhe! Yes, with these two problems addressed, the example works as intended.