Button disable/enable while performing action

Hi!

I have an app in which the user can interact through a button
When the button is pressed I execute a py script on the callback function and update a data table I have within a div

However I dont want the user to be able to press the button multiple times while the function is being executed.

So my desired behavior would be:

  1. user press button
  2. button gets disabled
  3. script starts to run
  4. script ends to run
  5. button is enabled
  6. another element gets updated with script output

I however havent found any way I can achieve this through dash so far

  • i cant have two call backs with the same output button.disabled

any ideas / advices?

Multiple outputs made this process much easier, luckily. I do this by using an intermediate trigger html.Div and associated function.

The buttonā€™s disabled property is only changed by the trigger function, which is triggered by clicking the button or by changing the trigger divā€™s value (which is only changed when the function is finished). The element function returns a trigger value as well as the element value.

The trigger function checks which thing triggered it. If it was the button, it returns False, but if it was the trigger, it returns True.

app.layout = html.Div([
    html.Button(id='button',disabled=False),
    # other element
    html.Div(id='other-element'),
    # trigger div
    html.Div(id='trigger',children=0, style=dict(display='none'))
])

@app.callback(
    Output('button','disabled'),
    [Input('button','n_clicks'),
     Input('trigger','children')])
def trigger_function(n_clicks,trigger):
    context = dash.callback_context.triggered[0]['prop_id'].split('.')[0]
    context_value = dash.callback_context.triggered[0]['value']
    
    # if the button triggered the function
    if context == 'button':
        # if the function is triggered at app load, this will not disable the button
        # but if the function is triggered by clicking the button, this disables the button as expected
        if n_clicks > 0 :
            return True
        else:
            return False
    # if the element function completed and triggered the function
    else:
        # if the function is triggered at app load, this will not disable the button
        # but if the function is triggered by the function finishing, this enables the button as expected
        return False

@app.callback(
    [Output('other-element','children'),
     Output('trigger','children')],
    [Input('button','n_clicks')])
def update_element(n_clicks):
    # do something
    return (
        'my element value', 
        1 # update the trigger value
    )

Hope this helps, do ask for clarification if needed! :rocket:

ohhh i didnt know we had multiple outputs now :open_mouth:

that makes life much easier

thx

1 Like

No prob. Take a look at the new releases, they have some cool features like dev tools, callback context, loading, no_update, multiple outputs, and more!

1 Like

Iā€™ve used your code with great success, but it doesnā€™t work anymore with the changes made to callbacks in 1.11.0. Do you have any idea how to adjust it?

That code should still be valid. Which errors are you getting?

I donā€™t get an error, but the buttons donā€™t reactivate when the action is finished, because the callback to trigger_function isnā€™t fired. I assume that this happens, because the value of ā€˜triggerā€™ doesnā€™t actually change (it is always 1), but Iā€™m not entirely sure.

Iā€™m having the same issue with the latest version of dash. This code reproduces the issue:

test_div = html.Div(id="test_div")
trigger_div = html.Div(id="trigger_div")
test_button = dbc.Button("test", id="test_button")


@app.callback(
    [
        Output(test_button.id, "disabled"),
    ],
    [Input(test_button.id, "n_clicks"), Input(trigger_div.id, "children")],
    [State(test_button.id, "disabled")],
)
def trigger_function(n_clicks, trigger, is_disabled):
    import logging
    logging.getLogger("test").info(f"trigger! n_clicks: {n_clicks}. trigger: {trigger}, is_disabled: {is_disabled}")
    if n_clicks is None and trigger is None:
        return [no_update]
    return [not is_disabled]

@app.callback(
    [
        Output(test_div.id, "children"),
        Output(trigger_div.id, "children")
    ],
    [Input(test_button.id, "n_clicks")],
)
def button_click(n_clicks):
    import logging
    logging.getLogger("test").info(f"n_clicks: {n_clicks}. Sleeping...")
    if n_clicks is None:
        return [no_update, no_update]
    import time
    time.sleep(10)
    return ["done", 1]
app.layout = html.Div([test_div, trigger_div, test_button])

The problem seems to be that dash wonā€™t execute the trigger_function while there is a callback running that has the trigger div as itā€™s output. When I remove the trigger div as input to the trigger_function callback (they both only have the button as input), then it executes both simultaneously.

2020-06-17 19:13:49,458 INFO:n_clicks: 1. Sleeping...
2020-06-17 19:13:59,510 INFO:trigger! n_clicks: 1. trigger: 1, is_disabled: None

Note there is no trigger! n_clicks: 1, trigger: None callback. Dash appears to have consolidated the n_clicks: 1, trigger: None and n_clicks: 1, trigger: 1 into a single callback.

I found a workaround using the new loading component. Unfortunately, I was not able to actually use the ā€œdisableā€ flag, which would be nice, but this achieves the desired effect by replacing the button with a spinner:

class ButtonDisableDuringCallbackDiv(html.Div):
    """Helper div to create a button that is associated with a long running
    computation. Linking a callback to the 'button_id' inside this div will
    cause the button to be disabled for the duration of the computation if the
    callback uses the trigger div as output:

        button_div = ButtonDisableDuringCallbackDiv("button", "Submit")

        @app.callback(
            Output(button_div.trigger_div.id, "children"),
            [Input(button_div.id)]
        )
        def handle_button_click(n_clicks):
            # do long compute
            import time
            time.sleep(10)
            # something must be returned to the trigger div
            return 1

    Args:
        name (str): Name used as the prefix for ids. Should be unique.
        display_label (str): String to display on the button.
    """

    def __init__(self, name, display_label):
        self.name = name
        self.trigger_div = html.Div(id=f"{name}_trigger_id", hidden=True)
        self.button = dbc.Button(
            children=display_label, id=f"{name}_button_id", color="primary", block=True
        )
        self.loading = dcc.Loading([self.button, self.trigger_div], type="circle")
        super().__init__(self.loading, id=f"{name}_div_id")

Hi I just learned about new matching features and thought those could help. Then, I realized that that may not be needed. Here I simply delete an existing button and create a new one in the function running time-consuming tasks, while another function is called to immediately disable the existing button.

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, MATCH, ALL
import time

# Note: commented-out lines use an alternative approach using dynanic index 


app = dash.Dash(__name__, suppress_callback_exceptions=True)

print('App is starting..')

app.layout = html.Div([
    html.Div(id='dynamic-button-container', 
    	children=[
    	html.Button(
    		#id={'type': 'dynamic-button', 'index': 0 },
    		id = 'button0',
    		children= 'Button'
    		)
    	]),
])

@app.callback(
    Output('dynamic-button-container', 'children'),
    [#Input({'type': 'dynamic-button', 'index': ALL}, 'n_clicks')
    Input('button0', 'n_clicks')
    ],
    [State('dynamic-button-container', 'children')])
def display_newbutton(n_clicks, children):
	#if n_clicks[0] is None: return children 
	if n_clicks is None: return children 
	else:
		print('Doing some calculation..') 
		time.sleep(3)

		new_element = html.Button(
		        #id={'type': 'dynamic-button','index': 0 }, #n_clicks[0] },
		        id = 'button0', 
		        children = 'Button' 
		    	)

		children.pop()
		children.append(new_element)
		print('Generating a new button')
		return children

# @app.callback(
#     Output({'type': 'dynamic-button', 'index': 0}, 'disabled'),
#     [Input({'type': 'dynamic-button', 'index': 0}, 'n_clicks')]
# )
@app.callback(
    Output('button0', 'disabled'),
    [Input('button0', 'n_clicks')]
)
def hide_newbutton(n_clicks):
	if n_clicks is None: return False
	else:
		print('Disabling the button')
		return True



if __name__ == '__main__':
    app.run_server(debug=True)

3 Likes

@russellthehippo thank you very much for posting this.

I couldnā€™t get this to work unfortunately - in my case the long calculation takes place inside the callback update_element - so the call to disable the button isnā€™t immediate but rather after the 20 seconds of when calc completes.

Do you have any suggestions on how to get this fixed?

Iā€™ve tried to setup another callback - but that hasnā€™t worked bc of the dash multiple callback outputs restriction

@app.callback(
    [Output('other-element','children'),
     Output('trigger','children')],
    [Input('button','n_clicks')])
def update_element(n_clicks):
    # VERY LONG CALC HERE
    return (
        'my element value', 
        1 # update the trigger value
    )
1 Like

The running argument of a callback makes this incredibly easy now: Advanced Callbacks | Dash for Python Documentation | Plotly