Dynamically button disabling not working properly

Hello! I have a button that runs a function that performs a request and returns a file. This can take some time.
I would like to dynamically disable the button when I press it and enable it automatically when function returns the file.
I’m able to disable and enable it programatically, but I’m not having the desired behavior: the button keeps enabled during the request and is disabled after the file is returned.

Button is declared like this:

html.Div([
    dbc.Button(
        "Generate pdf file",
        id=f"{PAGE_ID}_export_button",
        color='success',
        disabled=False,
        outline=True
    ),
    dcc.Download(id=f"{PAGE_ID}_download-pdf")
])

And I declared 2 callbacks, one for disabling button when it’s pressed, and another for processing and re-enabling button:

@app.callback(
    Output(f"{PAGE_ID}_export_button", "disabled"),
    [Input(f"{PAGE_ID}_export_button", "n_clicks")])
def disable(n_clicks):
    changed_id = [p['prop_id'] for p in callback_context.triggered][0]
    if changed_id != f"{PAGE_ID}_export_button.n_clicks":
        return no_update

    if n_clicks is None:
        return no_update
    return True

@app.callback(
    [Output(f"{PAGE_ID}_download-pdf", "data"),
    Output(f"{PAGE_ID}_export_button", "disabled")],
    Input(f"{PAGE_ID}_export_button", "n_clicks"),
    [State(f"{PAGE_ID}_labelsperserial", "value"),
    State(f"{PAGE_ID}_offsetlines", "value"),
    State(f"{PAGE_ID}_filename", "value"),
    State(f"{PAGE_ID}_start", "value"),
    State(f"{PAGE_ID}_devicetype", "value"),
    State(f"{PAGE_ID}_batchname", "value"),
    State(f"{PAGE_ID}_batchsize", "value"),
    State(f"{PAGE_ID}_revision", "value")],
    prevent_initial_call=True)
def download_pdf(n_clicks, labelsperserial, offsetlines, filename, start, devicetype, batchname, batchsize, revision):
    changed_id = [p['prop_id'] for p in callback_context.triggered][0]
    if changed_id != f"{PAGE_ID}_export_button.n_clicks":
        return [no_update, False]

    if n_clicks is None:
        return [no_update, False]
    
    fn = filename
    if fn is not None and not fn.endswith(".pdf"):
        fn = fn + ".pdf"
    response = requests.get(
        base_url + f"/labels/generate-microscopes-serial-sheet?labelsperserial={labelsperserial}&offsetlines={offsetlines}" +
            f"&filename={filename}&start={start}&devicetype={devicetype}&batchname={batchname}&batchsize={batchsize}&revision={revision}", 
    )
        
    return [dcc.send_bytes(response.content, fn, type="application/pdf"), True]

What I’m doing wrong?

Thanks for any ideas!

What version of dash are you running?

you cannot have multiple callbacks with the same output (ie:

Output("export_button" : "disabled")

This appears in both callbacks…

I believe you used to be able to do this, which is why I ask about the version…

Dash version is ==2.0.0
Yes, I’m using 2 callbacks with the same output, one for disabling the button when I press it, and another for function call and button re-enabling.
Is it possible to do that with just one callback?
Thanks for your help!

Refer to this link if you want to make it into two separate callbacks

So I’d do something like this:

You’ll need to use another component, really can be anything; something as simple as an html.Div with a children prop or dcc store or dcc interval would all be invisible to the user
Otherwise, you could use something like a dismissible alert

# Add this to your layout
dbc.Alert(
            "File Successfully Downloaded",
            id="alert",
            dismissable=True,
            is_open=False,
        )

@app.callback(
    Output("export_button", "disabled"),
    [Input("export_button", "n_clicks"),
    Input("alert", "is_open"])
def disable_enable(n_clicks,  alert):
    changed_id = [p['prop_id'] for p in callback_context.triggered][0]
    if changed_id == "export_button.n_clicks":
        if n_clicks is None:
            return no_update
        else:
            return True
    else:
        # changed_id should be alert.is_open
        # re-enable the button if the alert prop is triggered
        return False
        
@app.callback(
    [Output(f"{PAGE_ID}_download-pdf", "data"),
    Output("alert", "is_open")],
    Input(f"{PAGE_ID}_export_button", "n_clicks"),
    [State(f"{PAGE_ID}_labelsperserial", "value"),
    State(f"{PAGE_ID}_offsetlines", "value"),
    State(f"{PAGE_ID}_filename", "value"),
    State(f"{PAGE_ID}_start", "value"),
    State(f"{PAGE_ID}_devicetype", "value"),
    State(f"{PAGE_ID}_batchname", "value"),
    State(f"{PAGE_ID}_batchsize", "value"),
    State(f"{PAGE_ID}_revision", "value")],
    prevent_initial_call=True)
def download_pdf(n_clicks, labelsperserial, offsetlines, filename, start, devicetype, batchname, batchsize, revision):
    changed_id = [p['prop_id'] for p in callback_context.triggered][0]
    if changed_id != f"{PAGE_ID}_export_button.n_clicks":
        return [no_update, False]

    if n_clicks is None:
        return [no_update, False]
    
    fn = filename
    if fn is not None and not fn.endswith(".pdf"):
        fn = fn + ".pdf"
    response = requests.get(
        base_url + f"/labels/generate-microscopes-serial-sheet?labelsperserial={labelsperserial}&offsetlines={offsetlines}" +
            f"&filename={filename}&start={start}&devicetype={devicetype}&batchname={batchname}&batchsize={batchsize}&revision={revision}", 
    )
        
    return [dcc.send_bytes(response.content, fn, type="application/pdf"), True]

Let me know if this doesn’t work. I didn’t test this code, but I think it should work

1 Like