Skip to content
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

Determining Which Input Has Been Fired #291

Closed
chriddyp opened this issue Jul 13, 2018 · 15 comments
Closed

Determining Which Input Has Been Fired #291

chriddyp opened this issue Jul 13, 2018 · 15 comments
Assignees

Comments

@chriddyp
Copy link
Member

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:

  • Previous state
  • Multiple outputs
  • Keyword-style arguments instead of list-style arguments
  • Which input changed
  • Timestamps when inputs changed
  • Nested properties (e.g. subscribing to figure.layout.title)
  • Error handling (e.g. updating an error div container when an exception is thrown)

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

@ned2
Copy link
Contributor

ned2 commented Jul 15, 2018

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.

@chriddyp
Copy link
Member Author

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

@radekwlsk
Copy link

radekwlsk commented Aug 3, 2018

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.

@ned2
Copy link
Contributor

ned2 commented Aug 5, 2018

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.

@chriddyp
Copy link
Member Author

chriddyp commented Aug 8, 2018

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.

@mungojam
Copy link
Contributor

mungojam commented Jan 11, 2019

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

@T4rk1n T4rk1n self-assigned this Feb 14, 2019
@T4rk1n
Copy link
Contributor

T4rk1n commented Feb 14, 2019

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)

@hoangmt
Copy link

hoangmt commented Feb 14, 2019

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.

@alexcjohnson
Copy link
Collaborator

@hoangmt this solution has not been published to PyPI yet, it's a WIP at the two pull requests linked just above.

@hoangmt
Copy link

hoangmt commented Feb 14, 2019

@alexcjohnson my bad. Can't wait to see how it works. :D

@hoangmt
Copy link

hoangmt commented Mar 3, 2019

@alexcjohnson
Copy link
Collaborator

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’re a working conversation, so I wouldn’t want to be going back and sanitizing them after the fact.

@marwahaha
Copy link

This was helpful! Looks like it's been added to the "FAQs" here: https://dash.plot.ly/faqs

@thakkarparth007
Copy link

thakkarparth007 commented Mar 30, 2020

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"

@chriddyp
Copy link
Member Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants