Input two or more button - How to tell which button is pressed?

Hi Anton,

Ive tried several times (on mac), but I still end up with the same scenario. The components have the property n_clicks_previous but the value isn’t updated on click it is always None (even though the default is set to 0, n_clicks also starts as None). I tested it using the following:

app.layout = html.Div([
    html.Button('test', id='test'),
    html.Div(id='clicksDiv')
])

@app.callback(Output('clicksDiv', 'children'),
              [Input('test', 'n_clicks')],
               [State('test', 'n_clicks_previous')])
def show_clicks(n_clicks, n_clicks_previous):

    return 'clicks: {0} previous: {1}'.format(n_clicks, n_clicks_previous)

Will you be making a pull request to get this merged into master? If you do I can use the official build. I would like to know why I can’t get it working though!

Thanks

happy to do PR once I am back from trip in a couple of days

2 Likes

Any progress on this?

hi, it would appear that @rmarren1 commited this change 3 days ago -
https://github.com/plotly/dash-html-components/pull/37/commits/11ef50984bd29702e05ba80490b8acc41f50663b

one thing that I forgot to mention in my instruction (which is critical to build and install python egg file locally) (and having forgotten i spent last hour trying to recover that knowledge!) is that you need to run in the end:

npm run install-local

this needs to be run with su priviledge. this command will take results of npm install, wrap them into egg file, deinstall previous version of dash-html-components and install a modified one.

or wait till @chriddyp reviews the change and releases new version. hope wont take long.

My solution to this is to maintain click state for all buttons in a hidden div “clicked-button” then use this to determine which button was last clicked. The example below has three buttons delete, add and toggle.

import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_table_experiments as dt

app = dash.Dash()

app.layout = html.Div([
    html.Div([
        html.Button(id='add-button', children='Add', n_clicks=0),
        html.Button(id='del-button', children='Delete', n_clicks=0),
        html.Button(id='tog-button', children='Toggle', n_clicks=0),
        html.Div(id='clicked-button', children='del:0 add:0 tog:0 last:nan', style={'display': 'none'})
    ]),
    html.Div(id='display-clicked', children=""),
])

@app.callback(
    dash.dependencies.Output('display-clicked', 'children'),
    [dash.dependencies.Input('clicked-button', 'children')]

)
def button_action(clicked):

    last_clicked = clicked[-3:]

    if last_clicked == 'del':
        return "You clicked delete"
    if last_clicked == 'add':
        return "You clicked add"
    if last_clicked == 'tog':
        return "You clicked toggle"


@app.callback(
    dash.dependencies.Output('clicked-button', 'children'),
    [dash.dependencies.Input('del-button', 'n_clicks'),
     dash.dependencies.Input('add-button', 'n_clicks'),
     dash.dependencies.Input('tog-button', 'n_clicks')],
    [dash.dependencies.State('clicked-button', 'children')]
)
def updated_clicked(del_clicks, add_clicks, tog_clicks, prev_clicks):

    prev_clicks = dict([i.split(':') for i in prev_clicks.split(' ')])
    last_clicked = 'nan'

    if del_clicks > int(prev_clicks['del']):
        last_clicked = 'del'
    elif add_clicks > int(prev_clicks['add']):
        last_clicked = 'add'
    elif tog_clicks > int(prev_clicks['tog']):
        last_clicked = 'tog'

    cur_clicks = 'del:{} add:{} tog:{} last:{}'.format(del_clicks, add_clicks, tog_clicks, last_clicked)

    return cur_clicks


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

Thank you for this maral! I was trying to get something like this working using a similar strategy but didn’t realize that the State was different than the Input nor that the children had to be in string format!

Also see that you initialized n_clicks = 0, was having problems with “None” as the first entry. Lots to learn from this going forward!

I have a prerelease PR for this available in https://github.com/plotly/dash-html-components/pull/45. If it passes review, it should be released this week.

3 Likes

I’ve published the new versions now, here’s a quick example:

# Determine which input changed, or which button was clicked, using
# the latest functionality in dash-html-components (https://github.com/plotly/dash-html-components/pull/45)
import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
import datetime
import json

app = dash.Dash()

app.layout = html.Div([
    html.Button('Button 1', id='btn-1', n_clicks_timestamp='0'),
    html.Button('Button 2', id='btn-2', n_clicks_timestamp='0'),
    html.Button('Button 3', id='btn-3', n_clicks_timestamp='0'),
    html.Div(id='container')
])


@app.callback(Output('container', 'children'),
              [Input('btn-1', 'n_clicks_timestamp'),
               Input('btn-2', 'n_clicks_timestamp'),
               Input('btn-3', 'n_clicks_timestamp')])
def display(btn1, btn2, btn3):
    if int(btn1) > int(btn2) and int(btn1) > int(btn3):
        msg = 'Button 1 was most recently clicked'
    elif int(btn2) > int(btn1) and int(btn2) > int(btn3):
        msg = 'Button 2 was most recently clicked'
    elif int(btn3) > int(btn1) and int(btn3) > int(btn2):
        msg = 'Button 3 was most recently clicked'
    else:
        msg = 'None of the buttons have been clicked yet'
    return html.Div([
        html.Div('btn1: {}'.format(btn1)),
        html.Div('btn2: {}'.format(btn2)),
        html.Div('btn3: {}'.format(btn3)),
        html.Div(msg)
    ])

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

dash-determine-which-button-was-clicked

To use, upgrade to the latest dash-html-components and dash-renderer: https://dash.plot.ly/installation

The n_clicks_timestamp property is available for all elements in dash-html-components. We still need a solution that’s generally available for _all_components and all properties, but this is a start.

Thanks for the feedback everyone!

3 Likes

Thank you very much. The n_clicks_timestamp solved my problem too.

1 Like

Thanks for doing this change! finally i can upgrade to the latest dash-html-components.
One comment though, this approach is inferior to using n_clicks_previous. If you combine multiple n_clicks, n_clicks_timestamp and other inputs, then on other inputs change you wont be able to say whether button was clicked or not. Timestamps will still indicate which button was clicked latest, but change could come from other input. n_clicks_previous was very deterministic and would allow to to positively say that change in the input came from button and which one.

still, for most of the purposes n_clicks_timestamp works.

3 Likes

Sir, I can’t use n_clicks_time-stamp for other html components like, radio button or slide bar. It shows error-
TypeError: Unexpected keyword argument `n_clicks_timestamp’
Sorry sir, Issue resolved , I am trying it on dash_core_components. My bad

1 Like

Hope this will help

first put the n_timestamp value like this in button property

html.Button(
                [
html.I(className="fas fa-download margin_right", style={"margin": "0 13% 0 0"}), "CSV"],
                    className="btn btn-danger",
                    id="attribute-download_csv",
                    n_clicks_timestamp='0',
                    style={
                        "width": "81%"
                    }
                ),

@app.callback(
  Output('query_content', 'children'),
  [
        Input('phase_btn', 'n_clicks_timestamp'),
        Input('attribute_btn', 'n_clicks_timestamp'),
        Input('query_btn', 'n_clicks_timestamp'),
        Input('trends_chart', 'n_clicks_timestamp')
  ],
  state=[State("query_generated_text_box", "value")]
)
def display(btn1, btn2, btn3, btn4, query):
    btn_state = [int(btn1), int(btn2), int(btn3), int(btn4)]
    if all(v == 0 for v in btn_state):
        #it will be fired on th page load. 
        return "I was fired on page load"
    max_index = btn_state.index(max(i for i in btn_state if i is not None))
    if max_index == 0:
        return "one was clicked"
    elif max_index == 1:
        return "two was clicked"
    elif max_index == 2:
            return html.Div("Three was clicked)
    elif max_index == 3:
        return ("fouth was clicked")

To prevent the first callback use

raise dash.exceptions.PreventUpdate()

instead of

        #it will be fired on th page load. 
        return "I was fired on page load"

Agree here with n_click_timestamps being a stop gap solution which only works if all inputs are buttons. I have a hack which works when mixed with one other input by comparing the time-stamp to the current time (as opposed to each other) but that relies on a threshold which depends on latency between user and server… I’m going to look into using the hidden div trick for now.

Would be great to see dash properly fix the issue of multiple triggering inputs or allowing multiple callbacks with same output!

1 Like

Hi,
Thanks for providing the solution. I tried to use this function and it fixed my problem. But after running on a server for certain time, it give an error saying out of memory and crashed. Does the `n_clicks_timestamp’ just simply keep increasing? Thanks!

I ran into an issue whether I had to check whether a callback responded to a dcc.dropdown selection or a html.button click.

In the end I managed to get it to work by saving n_clicks in a hidden Div, and then loading this as a State n_clicks_previous. Then if n_clicks>n_clicks_previous I assume the button was pressed.

(basically a work around way of implementing the n_clicks_previous feature that was supposed to be implemented but no longer is?)

I found this to be a little bit of a nicer solution when you have many buttons:

@app.callback(Output('TESTTHIN', 'children'),
[Input('changes_all', 'n_clicks_timestamp'),
Input('changes_1y', 'n_clicks_timestamp'),
Input('changes_6m', 'n_clicks_timestamp'),
Input('changes_3m', 'n_clicks_timestamp'),
Input('changes_1m', 'n_clicks_timestamp'),
Input('changes_7d', 'n_clicks_timestamp'),
Input('changes_3d', 'n_clicks_timestamp'),
Input('changes_1d', 'n_clicks_timestamp'),])
def display(changes_all, changes_1y, changes_6m, changes_3m, changes_1m, changes_7d, changes_3d, changes_1d, *args, **kwargs):
    listedTimestamps = [changes_all, changes_1y, changes_6m, changes_3m, changes_1m, changes_7d, changes_3d, changes_1d]
    listedTimestamps = [0 if v is None else v for v in listedTimestamps]
    sortedTimestamps = sorted(listedTimestamps)

    if changes_all == sortedTimestamps[-1]:
        pickedButton = "changes_all"
    if changes_1y == sortedTimestamps[-1]:
        pickedButton = "changes_1y"
    if changes_6m == sortedTimestamps[-1]:
        pickedButton = "changes_6m"
    if changes_3m == sortedTimestamps[-1]:
        pickedButton = "changes_3m"
    if changes_1m == sortedTimestamps[-1]:
        pickedButton = "changes_1m"
    if changes_7d == sortedTimestamps[-1]:
        pickedButton = "changes_7d"
    if changes_3d == sortedTimestamps[-1]:
        pickedButton = "changes_3d"
    if changes_1d == sortedTimestamps[-1]:
        pickedButton = "changes_1d"
2 Likes

Hello Zoohair!
I have this same problem, trying to find whether a button or other inputs (‘children’ of divs in my case) triggered the callback.
Could you share your solution using current-time please?
Thanks!
Lionel

@lionelt

This is my solution: Note that I was using this on a local machine, and I’ve seen some oddities when it’s running on a server (timestamps might be using different t0, or the latency is more than I expected?)

Also this only works with one non-button event, I haven’t figured out how to do it with multiple ones.

@app.callback(
    Output('configMapInputs', 'children'),
    [Input('ResetClick', 'n_clicks_timestamp'),
     Input('UndoClick', 'n_clicks_timestamp'),
     Input('inputMap', 'selectedData')])
def serializeConfig(reset_time_ms, undo_time_ms, 
                    selectedData):
 
    #Hack to figure out what triggered this callback
    #Keep this here as the thresholds below
    #are based on assuming no computation is being done
    #in this callback before the deltas are computed
    curr_time_ms  = time.time()*1e3
    reset_time_ms = 0 if not reset_time_ms else reset_time_ms
    undo_time_ms  = 0 if not undo_time_ms else undo_time_ms
    reset_time_ms = curr_time_ms - reset_time_ms
    undo_time_ms  = curr_time_ms - undo_time_ms
    latency_thres = 500 #threshold between event being clicked and received at server
 

    if reset_time_ms < latency_thres:
        inputDists.clear()
    elif undo_time_ms < latency_thres:
        if len(inputDists) > 0: inputDists.pop()
    elif selectedData != None:
        newDist = None

using dash.callback_context to determine which input is fire,
see the FAQ https://dash.plot.ly/faqs