Skip to content

Updates the use of renderDashComponents to use ExternalWrapper when Dash 3 and falls back on dash-extensions for Dash 2#531

Merged
AnnMarieW merged 15 commits intosnehilvj:masterfrom
BSd3v:render-dash-components-update
Mar 20, 2025
Merged

Updates the use of renderDashComponents to use ExternalWrapper when Dash 3 and falls back on dash-extensions for Dash 2#531
AnnMarieW merged 15 commits intosnehilvj:masterfrom
BSd3v:render-dash-components-update

Conversation

@BSd3v
Copy link
Copy Markdown
Contributor

@BSd3v BSd3v commented Mar 13, 2025

The alteration will allow complex components to be mapped to allow callbacks from components.

eg:

import dash
from dash import Dash, _dash_renderer, clientside_callback, html, Output, Input, State, ctx, callback
import dash_mantine_components as dmc
_dash_renderer._set_react_version("18.2.0")

app = Dash()

min_step = 0
max_step = 3
active = 1

layout = html.Div(
    [
        dmc.Stepper(
            id="stepper-basic-usage",
            active=active,
            children=[
                dmc.StepperStep(
                    label="First step",
                    description=["Create an account",
                                 "test 2",
                                 html.H1(dmc.Button(id='rawr', children='test'), id='123', style={'color': 'red'})],
                    children=[
                        dmc.Text("Step 1 content: Create an account", ta="center"),
                    ],
                ),
                dmc.StepperStep(
                    label="Second step",
                    description="Verify email",
                    children=dmc.Text("Step 2 content: Verify email", ta="center"),
                ),
                dmc.StepperStep(
                    label="Final step",
                    description="Get full access",
                    children=dmc.Text("Step 3 content: Get full access", ta="center"),
                ),
                dmc.StepperCompleted(
                    children=dmc.Text(
                        "Completed, click back button to get to previous step",
                        ta="center",
                    )
                ),
            ],
        ),
        dmc.Group(
            justify="center",
            mt="xl",
            children=[
                dmc.Button("Back", id="back-basic-usage", variant="default"),
                dmc.Button("Next step", id="next-basic-usage"),
            ],
        ),
    ]
)



clientside_callback("""(n)=> {
    alert(`clicked ${n} times`)
}""",
Input('rawr', 'n_clicks'),
prevent_initial_call=True,
suppress_callback_exceptions=True)

clientside_callback(
    """(a) => {
        return a != 0
    }""",
    Output('rawr', 'disabled'),
    Input('stepper-basic-usage', 'active')
)

@callback(
    Output("stepper-basic-usage", "active"),
    Input("back-basic-usage", "n_clicks"),
    Input("next-basic-usage", "n_clicks"),
    State("stepper-basic-usage", "active"),
    prevent_initial_call=True,
)
def update(back, next_, current):
    button_id = ctx.triggered_id
    step = current if current is not None else active
    if button_id == "back-basic-usage":
        step = step - 1 if step > min_step else step
    else:
        step = step + 1 if step < max_step else step
    return step


app.layout = dmc.MantineProvider(
    layout
)

print(dash.__version__)

if __name__ == "__main__":
    app.run(debug=True)

In Dash 2, clicking on the button "rawr" doesnt trigger the callback, however, in Dash 3 you can see that this triggers.

@BSd3v
Copy link
Copy Markdown
Contributor Author

BSd3v commented Mar 13, 2025

Added the ability to handle async paths by helping to find context paths inside of different components. Here is an example that doesnt work on Dash 2 but works on Dash 3:


import dash_mantine_components as dmc
from dash_iconify import DashIconify
from dash import Dash, Input, Output, State, callback, _dash_renderer
_dash_renderer._set_react_version("18.2.0")

theme_toggle = dmc.ActionIcon(
    # [
    #     dmc.Paper(DashIconify(icon="radix-icons:sun", width=25), darkHidden=True),
    #     dmc.Paper(DashIconify(icon="radix-icons:moon", width=25), lightHidden=True),
    # ],
    variant="transparent",
    color="yellow",
    id="color-scheme-toggle",
    size="lg",
    ms="auto",
)


styles_css = """
.dmc-api-demo-root {
  border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
  padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
  border-radius: var(--mantine-radius-md);
  font-weight: 500;
  cursor: pointer;

  &[data-checked] {
    background-color: var(--mantine-color-blue-filled);
    border-color: var(--mantine-color-blue-filled);
    color: var(--mantine-color-white);
  }
}"""

demo_py = """
import dash_mantine_components as dmc

 dmc.Checkbox(
    classNames={"root": "dmc-api-demo-root"},
    label="Checkbox button",
    w=180
)"""


code = [
    {
        "fileName": "demo.py",
        "code": demo_py,
        "language": "python",
        "icon": DashIconify(icon="vscode-icons:file-type-reactts", width=20),
    },
    {
        "fileName": "styles.css",
        "code":styles_css,
        "language": "css",
        "icon": DashIconify(icon="vscode-icons:file-type-css", width=20),
    },
]


app = Dash(external_stylesheets=dmc.styles.ALL)


component = dmc.CodeHighlightTabs(
    code=code,
    withExpandButton=True,
    expandCodeLabel="Show full code",
    collapseCodeLabel="Show less",
    defaultExpanded=False,
    maxCollapsedHeight=100,
    m="lg"
)


app.layout = dmc.MantineProvider(
    [theme_toggle, dmc.Text("Your page content"), component],
    id="mantine-provider",
    forceColorScheme="light",
)


@callback(
    Output("mantine-provider", "forceColorScheme"),
    Input("color-scheme-toggle", "n_clicks"),
    State("mantine-provider", "forceColorScheme"),
    prevent_initial_call=True,
)
def switch_theme(_, theme):
    return "dark" if theme == "light" else "light"


if __name__ == "__main__":
    app.run(debug=True)

This stems from the fact that dash_iconify namespace doesnt exist with the icons commented out, Dash 3 can render these components because it uses the ExternalWrapper to target the prop specifically and uses dash-renderer to then pull the library.

Comment thread src/ts/props/dash.ts Outdated
component_name: string;
};
/** Holds the path of the component */
componentPath?: any[];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to do this without adding a new prop to every component?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it was just a placeholder, its sometimes in there but sometimes not... It's not necessary as far as I can tell.

Comment thread src/ts/props/dash.ts Outdated
Co-authored-by: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com>
@AnnMarieW AnnMarieW requested a review from alexcjohnson March 14, 2025 22:08
@alexcjohnson
Copy link
Copy Markdown
Collaborator

Looks good to me! Would be nice though to add a couple of tests, maybe just the two examples you show here plus one or two that already worked.

@BSd3v
Copy link
Copy Markdown
Contributor Author

BSd3v commented Mar 19, 2025

@alexcjohnson what are your thoughts? I think the fact that the button renders at all is a testament to Dash 2 style running in the stepper test.

Comment thread tests/test_optional_components.py Outdated
Comment on lines +208 to +212
if i:
until(lambda: dash_duo.find_element(f'svg.iconify:nth-child({i})').get_attribute('innerHTML') != old_html, timeout=3)
else:
until(lambda: dash_duo.find_element(f'svg.iconify:first-child').get_attribute('innerHTML') != old_html,
timeout=3) No newline at end of file
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:nth-child is 1-indexed, so should all of this be just :nth-child({i + 1})? Moreover, to be sure this test is really showing that the same element has changed, old_html and new should use the same selector, rather than old_html coming from the enumerate and new getting a new selector, something like:

for i in range(len(icons)):
    icon_selector = f'svg.iconify:nth-child({i + 1})'
    old_html = dash_duo.find_element(icon_selector).get_attribute('innerHTML')
    dash_duo.find_element(f'#test-{i}').click()
    until(lambda: dash_duo.find_element(icon_selector).get_attribute('innerHTML') != old_html, timeout=3)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not valid. I tried it...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

>  document.querySelector('div:first-child')===document.querySelector('div:nth-child(1)')
<- true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nth-child(2) isnt found for some reason, though it should be.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, dash iconify doesnt pass the id to the component...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexcjohnson what about what I just used? I wrapped the icons in divs and used that to target the components.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regardless as written this test is looking at the first one in both until calls, which is clearly not what you want. So one way or another we need to get the right elements, and get them in a symmetric way so it’s clear it’s the same element before and after

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I updated the test, that's what I was referring to. 🙂

Might be able to do without the time.sleep.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's nice and clear 😎

…s with rerenders and added test to address this
@BSd3v
Copy link
Copy Markdown
Contributor Author

BSd3v commented Mar 19, 2025

@alexcjohnson while further verifying the fix for @AnnMarieW original issue (forcing color switching the standard way) the codeHighlight was running into an issue, the props were being manipulated inplace. As Dash3 no longer rerenders the entire layout, this was causing an undefined error because ExternalWrapper didnt have a namespace.

Is the other function in danger of this, or is it different enough that it wont cause this issue? newRenderDashComponent is the singular.


Here is the image of the stores after the initial render in the old way:
image

and here after the adjustment:
image

Comment thread CHANGELOG.md Outdated
Co-authored-by: Ann Marie Ward <72614349+AnnMarieW@users.noreply.github.com>
@AnnMarieW
Copy link
Copy Markdown
Collaborator

Thanks @BSd3v 💃

@AnnMarieW AnnMarieW merged commit 4e3849b into snehilvj:master Mar 20, 2025
@AnnMarieW
Copy link
Copy Markdown
Collaborator

AnnMarieW commented Mar 23, 2025

@BSd3v Need to revisit this PR.

After this PR, It's not possible to hit the back button with the stepper as reported in #543

Also, the callbacks for the components as props don't work quite right. In this example, there is an input component in the description props in the second step. Try the following:

  1. Enter something into the input field in the second step. Note that the callback is triggered and shows the user value
  2. Hit the Next Step button
  3. Notice that the content of the input field is reset but the value of the callback is not.
  4. Notice the screen jiggle when moving between the steps
  5. Hit the back button to see the error (This happens even without the input component in the description)

dmc=1.1.0

from dash_iconify import DashIconify
import dash_mantine_components as dmc
from dash import Dash, _dash_renderer, Input, Output, State, callback, ctx
_dash_renderer._set_react_version("18.2.0")

app = Dash(external_stylesheets=dmc.styles.ALL)

min_step = 0
max_step = 3
active = 1


def get_icon(icon):
    return DashIconify(icon=icon, height=20)


component = dmc.Container(
    [
        dmc.Stepper(
            id="stepper-custom-icons",
            active=active,
            children=[
                dmc.StepperStep(
                     label="First step",
                     description="Create an account",
                    icon=get_icon(icon="material-symbols:account-circle"),
                ),
                dmc.StepperStep(
                    label="Second step",
                    description=dmc.Box(["verify email", dmc.TextInput("Go", id="btn")]),
                    icon=get_icon(icon="ic:outline-email"),
                    progressIcon=get_icon(icon="ic:outline-email"),
                    completedIcon=get_icon(
                        icon="material-symbols:mark-email-read-rounded"
                    ),
                    children=[dmc.Text("Step 2 content: Verify email", ta="center")],
                ),
                dmc.StepperStep(
                    label="Final step",
                    description="Get full access",
                    icon=get_icon(icon="material-symbols:lock-outline"),
                    progressIcon=get_icon(icon="material-symbols:lock-outline"),
                    completedIcon=get_icon(icon="material-symbols:lock-open-outline"),
                    children=[dmc.Text("Step 3 content: Get full access", ta="center")],
                ),
                dmc.StepperCompleted(
                    children=[
                        dmc.Text(
                            "Completed, click back button to get to previous step",
                            ta="center",
                        )
                    ]
                ),
            ],
        ),
        dmc.Group(
            justify="center",
            mt="xl",
            children=[
                dmc.Button("Back", id="back-custom-icons", variant="default"),
                dmc.Button("Next step", id="next-custom-icons"),
            ],
        ),
        dmc.Box(id="out")
    ]
)


@callback(
    Output("stepper-custom-icons", "active"),
    Input("back-custom-icons", "n_clicks"),
    Input("next-custom-icons", "n_clicks"),
    State("stepper-custom-icons", "active"),
    prevent_initial_call=True,
)
def update_with_icons(back, next_, current):
    button_id = ctx.triggered_id
    step = current if current is not None else active
    if button_id == "back-custom-icons":
        step = step - 1 if step > min_step else step
    else:
        step = step + 1 if step < max_step else step
    return step


@callback(
    Output("out", "children"),
    Input("btn", "value")
)
def update(n):
    return f"n={n}"

app.layout = dmc.MantineProvider(
    component
)

if __name__ == "__main__":
    app.run(debug=True)

@AnnMarieW
Copy link
Copy Markdown
Collaborator

fixed in #544 🎉
thanks @BSd3v we can move the discussion over there

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants