Dash 4 and html2pdf bug with radioitems

I have a small app that can create a screenshot of the webpage and save it as a pdf report. It uses a clientside callback and the html2pdf library to create the screenshot. Starting at Dash 4.0 there is a buggy interaction where dcc.RadioItems components have the current selection deselected when a screenshot is generated. I did some backwards testing with Dash 2.18 and 3.3 and the issue didn’t occur there.

To try out, load the included sample app and press the pdf report button. The screenshot generates. Then check the state of the radio items component in the dashboard. With Dash 2.18 and Dash 3.3 the last selected item is still selected. Now install Dash 4.0, after generating the screenshot, the radioitems selection gets unselected.

I looked in the html2pdf repo for issues on this and found 2 issues: issue 1 and issue 2, dating from 2019, but those suggestions don’t really help.

Any here that have some experience with html2pdf that have an idea what’s going on? Or any Dash 4 experts have an idea what change in the dcc library triggered this behavior?

Edit: I just saw that html2pdf has version 0.14.0 available from the Github page and I was still referring to the old 0.10.1 release. The issue still occurs with the latest version of html2pdf, so that also doesn’t resolve the issue. Unfortunately, the latest release is not available from CDN so to test the latest release you have to download the release and put the bundle file in the assets folder next to your app. If you do, you can remove the app.index_string statement from the sample app.

Solution

This issue was also addressed on Github. Final verdict is to either take the Patch solution suggested by @jinnyzor or return the value as a callback output as suggested in the GitHub issue.

Requirements

# Interaction ok
dash==2.18.1
plotly==5.24.0
dash-bootstrap-components==1.6.0
# Interaction ok!
dash==3.3.0
plotly==6.6.0
dash-bootstrap-components==2.0.4
# Interaction bad :(
dash==4.0.0
plotly==6.6.0
dash-bootstrap-components==2.0.4

Sample app

import dash
from dash import dcc, html, clientside_callback, Input, Output, State, callback
import dash_bootstrap_components as dbc

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

app.index_string = """
<!DOCTYPE html>
<html>
    <head>
        {%metas%}
        <title>{%title%}</title>
        {%favicon%}
        {%css%}
    </head>
    <body>
        {%app_entry%}
        <footer>
            {%config%}
            {%scripts%}
            <script type="text/javascript" 
                src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js" 
                integrity="sha512-GsLlZN/3F2ErC5ifS5QtgpiJtWd43JWSuIgh7mbzZ8zBps+dvLusV+eNQATqgA/HdeKFVgA5v3S/cIrLF7QnIg==" 
                crossorigin="anonymous" 
                referrerpolicy="no-referrer">
            </script>
            {%renderer%}
        </footer>
    </body>
</html>
"""
button_pdf_report = dbc.Button(
    id="button_download_report",
    children="Download PDF report",
    color="primary",
    outline=True,
    style={"marginLeft": "10px", "width": "200px"},
)

app.layout = dbc.Container(
    [
        html.H2("Radio Items & PDF Report", className="my-4"),
        dcc.RadioItems(
            id="radio-items",
            options=[
                {"label": "Option A", "value": "A"},
                {"label": "Option B", "value": "B"},
                {"label": "Option C", "value": "C"},
            ],
            value="A",
            labelStyle={"display": "block", "marginBottom": "8px"},
        ),
        html.Div(
            [
                dbc.Button(
                    id="button_show_value",
                    children="Show Selected Value",
                    color="secondary",
                    outline=True,
                    style={"width": "200px"},
                ),
                html.Div(id="selected-value-output", className="mt-2"),
            ],
            className="mt-4",
        ),
        html.Div(button_pdf_report, className="mt-4"),
    ],
    className="p-4",
    id="main_container",
    style={"minWidth": "1253px"}
)

clientside_callback(
    """
    function (button_clicked) {
        if (button_clicked && button_clicked > 0) {
            var mainContainerElement = document.getElementById("main_container");
            var main_container_width = parseInt(mainContainerElement.style.minWidth);

            var opt = {
                // margin units are those that are defined in jsPDF key below.
                margin: 10,
                filename: "test_file.pdf",
                image: { type: 'jpeg', quality: 0.98 },
                html2canvas: {
                    scale: 3,
                    width: main_container_width,
                    dpi: 300,
                },
                jsPDF: { unit: 'mm', format: 'A4', orientation: 'p' },
            };
            html2pdf().from(mainContainerElement).set(opt).save().then(function() {
                // Force a repaint of the radio items after pdf generation
                var radioEl = document.getElementById("radio-items");
                if (radioEl) {
                    radioEl.style.display = 'none';
                    // Reading offsetHeight forces the browser to flush layout
                    void radioEl.offsetHeight;
                    radioEl.style.display = '';
                }
            });
        }
    }
    """,
    Input(component_id="button_download_report", component_property="n_clicks"),
)

@callback(
    Output("selected-value-output", "children"),
    Input("button_show_value", "n_clicks"),
    State("radio-items", "value"),
    prevent_initial_call=True,
)
def show_selected_value(n_clicks, value):
    return f"Selected value: {value}"


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

I did some further debugging on the sample app and what I see is that the html of the RadioItems is not even altered by the html2pdf renderer. In the raw html, the item that was selected, remains selected, as can be seen from the aria-selected="true" in the raw html. So it seems just the visual of the selection isn’t (re)rendered properly after the screenshot.

Hello @Tobs

More than likely, I suspect that this could be due to some css settings somewhere, if you can find it, you might be able to print it.

You could also try to make sure that background images are turned on when printing the pdf.

I reviewed what I could in the css in the browser but I couldn’t find any difference there. If it’s inside the Dash 4 css files, then that would be some task to review.

I did try to let the AI analyze it issue as well, but it wasn’t able to make much out of it. The only direction that might make sense to me is related to a comment made in one of the GitHub issues: for me the issue was a "name" attribute of radio button

When I look at the html of RadioItems in Dash 4 vs Dash 3 then I see that in Dash 4 the RadioItems has a name attribute. (The raw html is attached below). In one of the AI analyses it says:

When html2canvas clones this input and inserts the clone into the document (even temporarily), the browser sees two <input type="radio"> elements with the same name. Checking/unchecking one in the clone can deselect the original, because radio buttons with the same name are in the same group browser-wide.

So it seems to hint that since the radio items all have the same name, it conflicts on the rerender of the page. Does this reasoning make any sense?

Dash 4

<div id="radio-items" class="dash-options-list dash-radioitems " role="listbox">
    <label class="dash-options-list-option selected" role="option" aria-selected="true">
        <span class="dash-options-list-option-wrapper">
            <input type="radio" name="radio-items" readonly="" class="dash-options-list-option-checkbox" value="A" checked="">
        </span>
        <span class="dash-options-list-option-text" style="display: block; margin-bottom: 8px;">
            <span>Option A</span>
        </span>
    </label>
    <label class="dash-options-list-option" role="option" aria-selected="false">
        <span class="dash-options-list-option-wrapper">
            <input type="radio" name="radio-items" readonly="" class="dash-options-list-option-checkbox" value="B">
        </span>
        <span class="dash-options-list-option-text" style="display: block; margin-bottom: 8px;">
            <span>Option B</span>
        </span>
    </label>
    <label class="dash-options-list-option" role="option" aria-selected="false">
        <span class="dash-options-list-option-wrapper">
            <input type="radio" name="radio-items" readonly="" class="dash-options-list-option-checkbox" value="C">
        </span>
        <span class="dash-options-list-option-text" style="display: block; margin-bottom: 8px;">
            <span>Option C</span>
        </span>
    </label>
</div>

Dash 3

<div id="radio-items">
    <label style="display: block; margin-bottom: 8px;" class="">
        <input class="" type="radio" checked="">Option A
    </label>
    <label style="display: block; margin-bottom: 8px;" class="">
        <input class="" type="radio">Option B
    </label>
    <label style="display: block; margin-bottom: 8px;" class="">
        <input class="" type="radio">Option C
    </label>
</div>

It works when I take the raw html and place it into codepen:

1 Like

Indeed, that is the key point. The raw html is correct, the css styling what I could see in the browser dev console is correct. Both show that Option A is selected. The problem is that after html2pdf is done creating the pdf screenshot, the dash app doesn’t visualize that the option is selected.

What I understood from the AI about the html2pdf library is that it takes a clone of the webpage and then renders the screenshot. The web page is then rerendered? It seems that with the changes to the dcc components in Dash 4 something in that process is creating a conflict. The AI was hinting to the name attribute as the root cause (since they are the same for all radio items) but honestly, it’s outside my knowledge area to assess whether that makes sense. So I am hoping to get more insights on where the root cause might lie.

I also opened a GitHub issue addressing this behavior to involve the html2pdf community as well.

For now, this will get it to refresh the render:

function (button_clicked) {
        if (button_clicked && button_clicked > 0) {
            var mainContainerElement = document.getElementById("main_container").cloneNode(true);
            var main_container_width = parseInt(mainContainerElement.style.minWidth);
            var opt = {
                // margin units are those that are defined in jsPDF key below.
                margin: 10,
                filename: "test_file.pdf",
                image: { type: 'jpeg', quality: 0.98 },
                html2canvas: {
                    scale: 3,
                    width: main_container_width,
                    dpi: 300,
                },
                jsPDF: { unit: 'mm', format: 'A4', orientation: 'p' },
            };
            html2pdf().from(mainContainerElement).set(opt).save().then(function() {
                var patch = new dash_clientside.Patch();
                dash_clientside.set_props("radio-items", {"value": patch.build()});
            });
        }
    }

Use this as the clientside function. I’m not entirely sure why the radio items arent refreshing properly, or why they are being cleared in the first place… My guess is something to do with the context of the radio items.


Almost looks like an issue with radio items in general, if you copy and add the whole items it drops the current checkbox, even in raw HTML.

Awesome, the Patch build indeed does the trick to rerender the component. It is a bit cumbersome in general design but at least this will fix the issue for now. Thanks @jinnyzor.

Secondly, I saw that you added the .cloneNode(true) for the mainContainerElement. I can make it work without it. Is that necessary?

Almost looks like an issue with radio items in general, if you copy and add the whole items it drops the current checkbox, even in raw HTML.

That is an intriguing comment. I did some further investigation because I didn’t understand what you meant by “copy and add the whole items”. What I found is this. If you run the sample app, open the browser dev console, and copy the outer html for the radio items component (the html that I shared above) if you:

  • Copy the outer HTML below the component so you get an exact duplicate of the component, the initial radio items selection is cleared. See the top RadioItems on the right side.
  • The name/id of the component appears in 2 different locations in the HTML. First at the start as id="radio-items", secondly in each item in the name attribute in the first span component. To run this second test, refresh the app so you get the original view again. Again copy the outer HTML for the original radio items component. Before duplicating the component, first edit the name attribute for each individual item, for example make it: name="radio-items2". You don’t have to change the id, leave it at <div id="radio-items". Now copy the original radio items. In this case the original radio items selection is not affected:

So it seems when a new radio items component is added with items with the same name as an already existing component, the existing components visual selection gets cleared.

I also tested if I duplicate just a single item in the existing radio items so it goes from 3 to 4 options, and that doesn’t change the visual, the current selection remains selected.

I’ll make a GitHub issue to address this to the team. Perhaps they can investigate if this is a Dash issue or a general react/html issue. It could indeed be that this name attribute is creating the conflict when html2pdf is generating the report. Perhaps somewhere with cloning the web page it re-adds the whole web page and the radio items experience the same behavior as above.

You dont need the clone, it was just from some troubleshooting process.

It’s possible that the htmlpdf issue is because it creates a clone of the object in order to render it and this causes the “last” added to take precedence.