-
Notifications
You must be signed in to change notification settings - Fork 220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Input elements to disable on click #864
Comments
HI @3tilley Have you tried using |
Thanks for the swift response! Yes we tried it, and for some reason it didn't behave reliably, as in the disabling on click didn't seem to be prompt. It seemed like the loading state still needed to be computed on the backend? Do you know if that's the case? |
Hello, I did a load of testing on this, and I wasn't able to use
All in all, it brought a lot of complexity to my demo app, and while it looked like was I wanted, it ultimately wasn't. I'm still of the mind that the way to do this is to the disabling into the Javascript of the button. Would you consider that PR? Demo Code
import logging
import os
from datetime import datetime
from uuid import uuid4
from dash import Dash, html, Input, Output, State
import dash_bootstrap_components as dbc
from dash.long_callback import DiskcacheLongCallbackManager
## Diskcache
import diskcache
DEFAULT_STATUS = "No clicks received"
launch_uid = uuid4()
cache = diskcache.Cache("./cache")
long_callback_manager = DiskcacheLongCallbackManager(
cache,
cache_by=[lambda: launch_uid],
expire=60,
)
logging.basicConfig(level=logging.DEBUG)
app = Dash(
"clicker",
external_stylesheets=[dbc.themes.BOOTSTRAP],
long_callback_manager=long_callback_manager,
prevent_initial_callbacks=True,
)
app.layout = html.Div(
[
dbc.Button("Standard button", id="btn-1"),
html.Div(id="status", children=DEFAULT_STATUS),
html.Div(id="token"),
dbc.Button("Long Callback", id="btn-2"),
html.Div(id="long-status", children=DEFAULT_STATUS),
html.Div(id="long-token"),
]
)
token = 0
last_click = None
# long_callback needs multiprocessing tools
TOKEN_KEY = "SHARED_TOKEN_KEY"
LAST_CLICK_TIMESTAMP = "LAST_CLICK_TIMESTAMP"
def log_values():
logging.info(f"{TOKEN_KEY}: {cache.get(TOKEN_KEY)}")
logging.info(f"{LAST_CLICK_TIMESTAMP}: {cache.get(LAST_CLICK_TIMESTAMP)}")
logging.info(f"Module executed on process: {os.getpid()}")
@app.callback(
Output("token", "children"),
Output("status", "children"),
Input("btn-1", "n_clicks_timestamp"),
State("token", "children"),
)
def display_click(n_clicks_timestamp, current_token):
logging.info(f"Regular callback triggered on {os.getpid()}")
global token
global last_click
msg = DEFAULT_STATUS
if n_clicks_timestamp:
dt = datetime.fromtimestamp(n_clicks_timestamp / 1000.0)
if last_click:
elapsed = (dt - last_click).total_seconds()
msg = f"Time since last click {elapsed}s. "
last_click = dt
if not current_token:
pass
elif int(current_token) == token:
msg += "Front end updated in time"
else:
msg += "WARNING: Token out of date"
token += 1
return str(token), html.Div(msg)
@app.long_callback(
Output("long-token", "children"),
Output("long-status", "children"),
Input("btn-2", "n_clicks_timestamp"),
State("long-token", "children"),
running=[(Output("btn-2", "disabled"), True, False)],
)
def callback(n_clicks_timestamp, current_token):
log_values()
logging.info(
f"long_callback triggered on {os.getpid()}. app object at: {id(app)}"
)
logging.debug(f"Front-end token: {current_token}")
logging.debug(f"Front-end timestamp: {n_clicks_timestamp}")
logging.debug(f"Token: {cache.get(TOKEN_KEY)}")
logging.debug(f"Last Click: {cache.get(LAST_CLICK_TIMESTAMP)}")
msg = DEFAULT_STATUS
if n_clicks_timestamp:
with diskcache.Lock(cache, LAST_CLICK_TIMESTAMP + "_LOCK"):
last_timestamp = cache.get(LAST_CLICK_TIMESTAMP)
dt = datetime.fromtimestamp(n_clicks_timestamp / 1000.0)
if last_timestamp:
long_last_click = datetime.fromtimestamp(last_timestamp)
elapsed = (dt - long_last_click).total_seconds()
msg = f"Time since last click {elapsed}s. "
cache.set(LAST_CLICK_TIMESTAMP, dt.timestamp())
with diskcache.Lock(cache, TOKEN_KEY + "_LOCK"):
token = cache.get(TOKEN_KEY)
if n_clicks_timestamp:
if not current_token or not n_clicks_timestamp:
pass
elif int(current_token) == token:
msg += "Front end updated in time"
else:
msg += "WARNING: Token out of date"
token += 1
cache.set(TOKEN_KEY, token)
logging.debug(f"Token: {token}")
log_values()
return str(token), html.Div(msg)
if __name__ == "__main__":
# Cache has to be set here as long_callback reloads main.py every time
cache.set(TOKEN_KEY, 0)
cache.set(LAST_CLICK_TIMESTAMP, None)
log_values()
app.run(debug=True) Collapsible until here. Screenshot: |
Hi all,
We have an issue whereby multiple clicks of a button get sent to the backend before we can respond and disable the button in a callback. Imagine a button that you only want to be allow to get pressed once before the server has responded with some updated data. Perhaps a form with some values and an "Execute Trade" button. You would like to make sure the trade is sent once, portfolios and prices are calculated in dash and then updated on the client, before allowing them to press again. You might instead disable the button until the trade has totally completed.
We have explored different options on the backend, from opening a transaction block, to requiring a token that represents the latest update, but all of these methods are a bit fiddly, and offer a poor UX. What we would like to happen would be a button that upon clicking, disables itself, and then it's the server's job to renable that when it sees fit.
I can't see a way of doing this faster than someone can double-click other than doing it in the Javascript. I envisage something like
dbc.Button(id="my-button", disable_on_click=True)
. You could do something more advanced like disabling automatically until completion of the callback, but perhaps it's clearer to just make the callback returnOutput("my-button", "disabled")
.This is more of a question for dash than dbc I guess, but I think that
LoadingState
has to be engaged by the backend, so wouldn't work here?Unless there is a workaround, does this seem useful? If so, could anyone offer me guidance on how to implement this? I'm a confident Python dev but an intimidated Javascript one. If there is some prior art I'm happy to take that and try and learn from it.
Thanks!
The text was updated successfully, but these errors were encountered: