Next/previous dropdown value with button click?

Hi. I have a dropdown in my app and I would like to add two buttons that when pressed would move the dropdown to the next or previous value in the list. This would give the user a way to scroll through the list instead of opening the dropdown to select the next value each time. Is this possible?

Thanks.

Hi @k403

Yes, it is possible, you need to use a callback that use as Input the button (n_clicks), as State the dropdown value, and as Output the dropdown value. Perhaps you also need to have as State the dropdown options (if it is not a fix list that you know)

the function inside the callback must check if the button was trigger and get the current value of the dropdown, then find the possition of the value in the dropdown options, and select the next or the previous element of the list and return it as a value of the dropdown.

Thanks! I got this working with a single “next” button but when I add another “prev” button I am getting some strange behaviour.

Here is a simple app that re-creates the issue I am having. In this case the next button doesn’t do anything and when the prev button is clicked the next callback is being called. Why doesn’t this work?

import dash
import dash_html_components as html
from dash.dependencies import Input, Output

app = dash.Dash()

app.layout = html.Div(
    [
        html.Button(id="prev", children="prev", n_clicks=0),
        html.Button(id="next", children="next", n_clicks=0),
        html.Div(id="text"),
    ]
)

@app.callback(Output("text", "children"), [Input("prev", "n_clicks")])
def prev(n_clicks):
    if n_clicks != 0:
        return f"prev {n_clicks}"

@app.callback(Output("text", "children"), [Input("next", "n_clicks")])
def next(n_clicks):
    if n_clicks != 0:
        return f"next {n_clicks}"

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

This is because at the moment, you can’t target a given Dash component as “Output” via more than one input/callbacks. When you try to run your sample code, you should see this error message on page load:

In the callback for output(s):
text.children
Output 0 (text.children) is already in use.
Any given output can only have one callback that sets it.
To resolve this situation, try combining these into
one callback function, distinguishing the trigger
by using dash.callback_context if necessary.

To get around this, you can either use the “multiple inputs” approach documented here: Part 2. Basic Callbacks | Dash for Python Documentation | Plotly

Or take a look at this work from Emil: Multiple callbacks for an output - #4 by DeejL

1 Like

I didn’t see that error message but thanks for the help! This worked:

@app.callback(
    Output("text", "children"), [Input("prev", "n_clicks"), Input("next", "n_clicks")]
)
def button(prev_clicks, next_clicks):
    changed_id = [p['prop_id'] for p in dash.callback_context.triggered][0]

    if prev_clicks != 0 or next_clicks !=0:
        if 'prev' in changed_id:
            return f"prev {prev_clicks}"
        if 'next' in changed_id:
            return f"next {next_clicks}"

Another question regarding dash.callback_context usage. In the dash documentation example:

@app.callback(Output('container-button-timestamp', 'children'),
              Input('btn-nclicks-1', 'n_clicks'),
              Input('btn-nclicks-2', 'n_clicks'),
              Input('btn-nclicks-3', 'n_clicks'))
def displayClick(btn1, btn2, btn3):
    changed_id = [p['prop_id'] for p in dash.callback_context.triggered][0]
    if 'btn-nclicks-1' in changed_id:
        msg = 'Button 1 was most recently clicked'
    elif 'btn-nclicks-2' in changed_id:
        msg = 'Button 2 was most recently clicked'
    elif 'btn-nclicks-3' in changed_id:
        msg = 'Button 3 was most recently clicked'
    else:
        msg = 'None of the buttons have been clicked yet'
    return html.Div(msg)

The else clause initially triggered because none of the buttons have been pushed yet.

However in my code this doesn’t seem to work. Upon the app initially loading next is in changed_id without a button being pushed. I had to use the if prev_clicks != 0 or next_clicks !=0: test to get around this issue.

The reason for the behavior you’re observing is because when a button object renders for the first time (imagine Page load event), it isn’t in “clicked” state at all. So the corresponding “n_clicks” value by default is “None” (the Python None type). However, you’re overriding it by associating a “non-None” value of 0 to each button in your app.layout.

If you want to continue to set the initialization values to 0, your current logic will work and that’ll be correct. However, if you want to use the Dash default behavior (by removing n_clicks=0 parts, then in the calllback you will need to use Python None check and take action if the values are None or not None based on your need.

This is the same issue that I ran into:

https://community.plotly.com/t/bug-in-callback-context/31621/2

The example works on the documentation website but when I run it in my own environment “Button 3 was most recently clicked” shows upon the app loading.