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
https://www.dash-extensions.com/
MIT License
409 stars 58 forks source link

`PrefixIdTransform` does not work with `DashBlueprint` #243

Closed wuyuanyi135 closed 1 year ago

wuyuanyi135 commented 1 year ago

Please see the code modified from the PrefixIdTransform and embedding. I want to use the transform to prevent id collision when using a DashBlueprint as a layout component. However it only transforms the callback, not the components, hence giving error ID not found in layout.

Is this an expected behavior?

from dash_extensions.enrich import Output, DashProxy, Input, html, PrefixIdTransform, DashBlueprint

bp = DashBlueprint(transforms=[PrefixIdTransform(prefix="some_prefix")])
bp.layout = html.Div([html.Button("Click me", id="btn"), html.Div(id="log")])

@bp.callback(Output("log", "children"), Input("btn", "id"))
def func(btn_id):  # argument is omitted from the function
    return f"The button id is {btn_id}"

if __name__ == "__main__":
    app = DashProxy(__name__)
    app.layout = html.Div(
        [
            html.H1("Test"),
            bp.layout
        ]
    )
    bp.register_callbacks(app)
    app.run_server(debug=True)
wuyuanyi135 commented 1 year ago

I have a workaround here. Instead of using DashBlueprint.layout, the DashBlueprint._layout_value() will invoke the transformation for the layout. This seems like an ugly hack and I am not sure if this should be used.

from dash_extensions.enrich import Output, DashProxy, Input, html, PrefixIdTransform, DashBlueprint

bp = DashBlueprint(transforms=[PrefixIdTransform(prefix="some_prefix")])
bp.layout = html.Div([html.Button("Click me", id="btn"), html.Div(id="log")])

@bp.callback(Output("log", "children"), Input("btn", "id"))
def func(btn_id):  # argument is omitted from the function
    return f"The button id is {btn_id}"

if __name__ == "__main__":
    app = DashProxy(__name__)
    app.layout = html.Div(
        [
            html.H1("Test"),
            bp._layout_value()
        ]
    )
    bp.register_callbacks(app)
    app.run_server(debug=True)
emilhe commented 1 year ago

Ah, yes, this is a bug in the documentation. With the current code, the correct way to embed the layout is indeed,

bp._layout_value()

I guess what is needed here is a more proper syntax, i.e. an alias for _layout_value(); maybe an embedfunction or property? So the code would be,

bp.embed()

What do you think? Which syntax would you prefer?

wuyuanyi135 commented 1 year ago

Hi, @emilhe, thanks for the confirmation. Yes, I agree having a proper alias is helpful. Is it possible to make use of the layout property instead of creating a new one? It seems like an easy mistake and it fails sliently, making it a bit hard to debug. Maybe raise a warning when transformation is not applied?

emilhe commented 1 year ago

@wuyuanyi135 since the blueprint was designed to mirror Dash object itself, the intention of the layout property is to represent the unmodified layout, i.e. without any transforms applied. Hence, I would prefer to use a different property/function for the layout including modifications. What about a function that registers the callbacks and returns the resulting layout? I.e. your example would be,

from dash_extensions.enrich import Output, DashProxy, Input, html, PrefixIdTransform, DashBlueprint

bp = DashBlueprint(transforms=[PrefixIdTransform(prefix="some_prefix")])
bp.layout = html.Div([html.Button("Click me", id="btn"), html.Div(id="log")])

@bp.callback(Output("log", "children"), Input("btn", "id"))
def func(btn_id):  # argument is omitted from the function
    return f"The button id is {btn_id}"

if __name__ == "__main__":
    app = DashProxy(__name__)
    app.layout = html.Div(
        [
            html.H1("Test"),
            bp.embed(app)
        ]
    )
    app.run_server(debug=True)
wuyuanyi135 commented 1 year ago

@emilhe I see. I agree having an embed function handling both layout and callback registration will be very useful!

emilhe commented 1 year ago

I have made the first draft of an implementation and pushed an rc version,

pip install dash-extensions==0.1.12rc1

which enables code like the example posted above. Could you test if it works for your case(s) as well?

wuyuanyi135 commented 1 year ago

@emilhe Sorry for the late response. I have tested 0.1.12rc1 and 0.1.12. The proposed embed works well when using eager-loaded layout: (app.layout = ....). However, if lazy-loaded layout is used (app.layout=lambda : .....) The embed did not properly register the callbacks.

MWE with your example:

from dash_extensions.enrich import Output, DashProxy, Input, html, PrefixIdTransform, DashBlueprint

bp = DashBlueprint(transforms=[PrefixIdTransform(prefix="some_prefix")])
bp.layout = html.Div([html.Button("Click me", id="btn"), html.Div(id="log")])

@bp.callback(Output("log", "children"), Input("btn", "id"))
def func(btn_id):  # argument is omitted from the function
    return f"The button id is {btn_id}"

if __name__ == "__main__":
    app = DashProxy(__name__)
    app.layout = lambda: html.Div(
        [
            html.H1("Test"),
            bp.embed(app)
        ]
    )
    app.run_server(debug=True)

For this case, the only way to make it work is to register the callback first then use _layout_value() in the layout function.

emilhe commented 1 year ago

Ah, yes, I didn't test for function layouts. I have made an initial implementation that targets this issue,

pip install dash-extensions==0.1.13rc1

I still need to do a bit more testing before merging the fix into master. Does it solve your issues?

wuyuanyi135 commented 1 year ago

@emilhe Thanks. For my simple tests, this version works fine now! Closing this issue.