Search code examples
javascriptpythonasynchronousplotly-dash

How to disable a button for an amount of seconds in a Dash client side callback?


In a Dash 2.11 application I need to disable a button for 5 seconds. Here is the definition of my button:

                            dbc.Button(
                                "Expand",
                                id=expansion_button_id,
                                className="primary-btn mt-2 btn-small",
                                n_clicks=0,
                                disabled=False,
                            ),

Here is the definition of the client side callback:

        self.app.clientside_callback(
            ClientsideFunction(namespace="foo", function_name="toggle_expand_button"),
            Output(self._expand_id, "disabled"),
            [
                Input(self._expand_id, "id"),
                Input(self._expand_id, "n_clicks")
            ],
        )

and this is my JavaScript function:

    toggle_expand_button: function(component_id, n_clicks) {
        console.log("component_id: " + component_id)
        console.log("n_clicks: " + n_clicks)

        if (!n_clicks) {
            return false
        }

        if (n_clicks > 0) {
            setTimeout(function() {
                document.getElementById(component_id).disabled = false;
            }, 5000);  // 5 seconds
            return true;
        }

        return true
    }

The button works as expected until the 5 seconds are over, but after this point no click action is fired even though the button does no longer appear disabled. What's wrong with my code?


Solution

  • The problem is that from Dash's perspective your dbc.Button component's disabled property value is still True.

    In Dash apps, the dash-renderer project...contains all of the "state" of the application and it passes those properties into the individual components. When a component's properties change through user interaction (e.g. typing into an or hovering on a graph), the component needs to call setProps with the new values of the property. Dash's frontend (dash-renderer) will then rerender the component with the new property and make the necessary API calls to Dash's Python server callbacks.

    https://dash.plotly.com/react-for-python-developers

    Dash is not notified when you make changes to the DOM directly like you do here:

    setTimeout(function() {
        document.getElementById(component_id).disabled = false; // Change not tracked by Dash
    }, 5000);  // 5 seconds
    

    What you could do instead is something like this:

    from dash import Dash, html, dcc, ctx
    from dash.dependencies import Output, Input
    import dash_bootstrap_components as dbc
    
    
    app = Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])
    app.layout = html.Div([
      dcc.Interval(id="interval", interval=5000, disabled=True),
      dbc.Button(
        id="my-button",
        children="Expand",
        className="primary-btn mt-2 btn-small"
      )
    ])
    
    
    @app.callback(
      Output("interval", "disabled"),
      Output("my-button", "disabled"),
      Input("interval", "n_intervals"),
      Input("my-button", "n_clicks"),
      prevent_initial_call=True
    )
    def toggle_expand_button(n_intervals, n_clicks):
      enable_button = (True, False)  
      disable_button = (False, True)
    
      if ctx.triggered_id == "interval":
        return enable_button
      
      return disable_button
    
    
    if __name__ == "__main__":
      app.run_server()
    

    The idea above is to use an interval and make it disabled by default. Then when we click the button we make the button disabled but the interval enabled. Now the interval is enabled and it waits 5000 milliseconds after which the callback triggered again and we disable the interval and enable the button again.