We need a way to determine which input has changed. We've provided a temporary hack with n_clicks_timestamp but we need something that is more general.
I'm not worried about the implementation, it fits into the dash-renderer architecture nicely.
What's not as clear to me is what the app.callback decorated functions should look like. How does this interface scale when we want to add more things like:
figure.layout.title)So, let's use this thread to just propose different interfaces to the callbacks. Of course, doing this in a way that would be backwards compatible is preferred.
cc @plotly/dash
Just spitballing here, not actually sure how much I'm into this. One approach could be to use a request object that is passed into the callback functions, as is common with many web frameworks, and then retrieve the component registrations from the request using element_id.prop_name identifiers. The request object could track which element fired and the individual component objects could track things like has_changed and prev_value. eg
app.layout = html.Div([
html.Div(id='target'),
dcc.Input(id='my-input', type='text', value=''),
html.Button(id='submit1', n_clicks=0, children='Submit 1')
html.Button(id='submit2', n_clicks=0, children='Submit 2')
])
@app.callback([Output('target', 'children')],
[Input('submit', 'n_clicks'), Input('submit2', 'n_clicks')],
[State('my-input', 'value')])
def callback(request):
my_input = request.state['my-input.value']
if my_input.has_changed:
result = f"Input {request.trigger.id} triggered callback; {my_input.id} changed value from {my_input.prev_value} to {my_input.value}"
else:
result = f"Input {request.trigger.id} triggered callback; {my_input.id} did not change value."
return result
This also solves the problem of managing unwieldy lists of Input/State that you need to align with the callback function arguments, as I describe as being an issue in #159.
However this would likely mean either a non-backwards compatible change to callback function signatures, or we have two callback functions, the previous simple list of argument values alongside the the new request-based one.
However this would likely mean either a non-backwards compatible change to callback function signatures, or we have two callback functions, the previous simple list of argument values alongside the the new request-based one.
One option at our disposal is checking the number or even the type of arguments in the def my_callback function from our decorator and then passing a new set of arguments through. We could also check the types and number of arguments passed into our app.callback function.
That is, roughly:
def callback(output, inputs, states):
def scoped_wrapper(func):
def wrapper(*args, **kwargs):
if len(args) == 1 and (len(inputs) + len(states) > 1)
# e.g. callback signature type 1
request = {
'inputs': inputs,
'states': states
}
func(request)
else:
# e.g. existing callback signature
return func(*(inputs + states))
In your example, we'd some way to differentiate between a callback with a single input and a callback with a single input that uses the request object
Why not make 2 options available for the user to choose from?
With
@app.callback(output, inputs, states, as_request=True)
assume signature request where as_request=False is default. This way if there are more arguments with as_request=True exception could be raised to inform the user that he mistakenly used wrong signature.
It may be also good idea to allow configuring Dash object with default Dash(..., callbacks_with_request=False) but allowing to set as_request=app.callbacks_with_request allowing user to define his choice upfront.
One option at our disposal is checking the number or even the type of arguments in the def my_callback function from our decorator and then passing a new set of arguments through. We could also check the types and number of arguments passed into our app.callback function.
Ah, good point @chriddyp. Polymorphism through decorators! If we went down the path of a request-like context object, this could well be a good approach to supporting it alongside the original callback signature.
Why not make 2 options available for the user to choose from?
Note that in general, I'm looking for solutions that are unified and ideally backwards compatible. As in the zen of python, there should be one way to do things.
You could add another optional parameter to the callback decorator: PreviousState() which would feed the previous state to the function, not sure what that would look like, but once you have previous state then you can work out which one or many things have changed
Got a prototype working...
import dash
import dash_html_components as html
from dash.dependencies import Output, Input
from dash.exceptions import PreventUpdate
app = dash.Dash(__name__)
BUTTONS = ['btn-{}'.format(x) for x in range(1, 6)]
app.layout = html.Div([
html.Div([
html.Button(x, id=x) for x in BUTTONS
]),
html.Div(id='output'),
])
@app.callback(Output('output', 'children'),
[Input(x, 'n_clicks') for x in BUTTONS])
def on_click(*args):
if not dash.callback.triggered:
raise PreventUpdate
trigger = dash.callback.triggered[0]
input_value = dash.callback.inputs.get(trigger)
return 'Just clicked {} for the {} time!'.format(trigger, input_value)
if __name__ == '__main__':
app.run_server(debug=True, port=9091)
Hi @T4rk1n,
Look like a very good example. I have tried it but i got this error message: line 22, in on_click
if not dash.callback.triggered:
AttributeError: module 'dash' has no attribute 'callback'.
Do you know how to fix it? I am using version '0.37.0' of dash.
@hoangmt this solution has not been published to PyPI yet, it's a WIP at the two pull requests linked just above.
@alexcjohnson my bad. Can't wait to see how it works. :D
Should the above example by @T4rk1n replaced by this https://github.com/plotly/dash-docs/blob/87c7afd2267bc4b195a1c61ed2c422b043485502/tutorial/examples/faqs/last_clicked_button.py?
You mean the change from dash.callback to dash.callback_context? Yes, the example here is out of date. But GitHub issues and PR comments are not documentation, they鈥檙e a working conversation, so I wouldn鈥檛 want to be going back and sanitizing them after the fact.
This was helpful! Looks like it's been added to the "FAQs" here: https://dash.plot.ly/faqs
It seems to be missing from the FAQs now. However, I do find this information here (https://dash.plotly.com/advanced-callbacks) under "Determining which Input has fired with dash.callback_context"
Most helpful comment
Got a prototype working...