RenaudLN / dash-pydantic-form

MIT License
31 stars 6 forks source link

form data in a dash.Output does not work #40

Closed lumpi101 closed 2 weeks ago

lumpi101 commented 2 weeks ago

Using forms as State or Input in a dash.callback works fine. Using it as Output does not work. The layout will be rendered just fine but no callback works and I see a "Overlapping wildcard callback outputs" error in the browser logs.

Below is an example. The second callback causes the bug. If I remove it the other callback will work again. I also tried a allow_duplicate=True which also did not work.

I realized that there is no example in any tutorial where form data is used as Output. So probably it's rather a feature request than a bug report.

import dash_mantine_components as dmc
import dash
from dash import Input, Output, State, callback, html
from dash_pydantic_form import ModelForm
from pydantic import BaseModel, Field, ValidationError

email_regex = r"^\S+@\S+\.\S+$"

class LoginData(BaseModel):
    email: str = Field(
        title="Email",
        description="Work email only, no gmail allowed",
        pattern=email_regex,
    )
    password: str = Field(title="Password", description="Make it strong", min_length=6)

stack = dmc.Stack(
    [
        ModelForm(LoginData, "login", "auto", submit_on_enter=True),
        html.Div(dmc.Button("Submit", id="submit", mt=-10)),
        html.Div(dmc.Button("Prefill", id="prefill", mt=-10)),
        html.Div(id="output"),
    ]
)

@callback(
    Output("output", "children"),
    Output(ModelForm.ids.errors("login", "auto"), "data"),
    Input("submit", "n_clicks"),
    Input(ModelForm.ids.form("login", "auto"), "data-submit"),
    State(ModelForm.ids.main("login", "auto"), "data"),
    prevent_initial_call=True,
)
def check_form(_trigger: int, _trigger2: int, form_data: dict):
    try:
        LoginData.model_validate(form_data)
    except ValidationError as exc:
        errors = {":".join([str(x) for x in error["loc"]]): error["msg"] for error in exc.errors()}
        return dmc.Text("Try again", fw=500, c="red"), errors

    return dmc.Text("Form is valid", c="green"), {}

@callback(
    Output(ModelForm.ids.main("login", "auto"), "data"),
    Input("prefill", "n_clicks"),
    prevent_initial_call=True,
)
def prefill(_trigger: int):
    return LoginData(email="foo@mail.com", password="abcabc").model_dump()

dash._dash_renderer._set_react_version('18.2.0')
app = dash.Dash(__name__, external_stylesheets=dmc.styles.ALL)
app.layout = dmc.MantineProvider(stack)

if __name__ == '__main__':
    app.run_server()
RenaudLN commented 2 weeks ago

As you say this is not currently supported. An easy workaround however is to target the parent of the form in the callback and replace it with a new one.

lumpi101 commented 2 weeks ago

That works very well for me. Thanks a lot!