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

[Feature Request] Ability to specify order of components when using pattern-matching wildcards (ALL) #2834

Open
celia-lm opened this issue Apr 16, 2024 · 1 comment

Comments

@celia-lm
Copy link

When pattern matching callbacks are used with 2 or more key-value pairs and the ALL keyword, Dash passes the list of Inputs to the callback function in the order in which they are created, instead of listing all of the elements of the first group, then the second group, etc. (see full examples at the end of the issue)

For example, if we create something with ids like:

{'type': 'button', 'card_number': card_idx, 'index': i}

and we use ALL for both card_idx and i in the callback decorator (to get their values/children/whatever), we get a list, that summarised looks like:

['card0_button0', 'card0_button1', 'card1_button0', 'card1_button1']

which is easy enough to work with. However, if buttons are added to the first card (or any except the last), then the order will be:

['card0_button0', 'card0_button1', 'card1_button0', 'card1_button1', 'card0_button2'] 

The expected output would be:

['card0_button0', 'card0_button1', 'card0_button2', 'card1_button0', 'card1_button1'] 

Describe the solution you'd like

Something like a sort_by argument for Input/Output/State that allows developers to specify the id dict key they want to use to sort the Inputs/States when using ALL.

Input({'type': 'button', 'card_number': card_idx, 'index':ALL}, 'n_clicks', sort_by='card_number')

It could also be a list, like:

Input({'type': 'button', 'card_number': card_idx, 'index':ALL}, 'n_clicks', sort_by=['card_number', 'index'])

Sample apps

To replicate current behaviour:

import dash
from dash import Dash, dcc, html, Input, Output, State, callback
from dash import ALL, MATCH, Patch, ctx

app = Dash(__name__)

app.layout = html.Div([
    html.Div(
        id={'type': 'card', 'card_number': card_idx},
        children = [
            html.Button(
                id={'type': 'add', 'card_number': card_idx},
                children='Add new button'
            ),
            html.Button(
                id={'type': 'button', 'card_number': card_idx, 'index': 0},
                children=f"card{card_idx}_button0"
            )
    ]) for card_idx in range(1,4)
] + [
    html.Div(id='out', children='No button clicked yet')
])

@callback(
    Output({'type': 'card', 'card_number': MATCH}, 'children'),
    Input({'type': 'add', 'card_number': MATCH}, 'n_clicks'),
    prevent_initial_call=True
)
def add_button_to_card(n_clicks):
    card_idx = ctx.triggered_id['card_number']
    card_children = Patch()
    card_children.append(
        html.Button(
                id={'type': 'button', 'card_number': card_idx, 'index': n_clicks},
                children=f"card{card_idx}_button0"
            )
    )
    return card_children

@callback(
    Output('out', 'children'),
    Input({'type': 'button', 'card_number': ALL, 'index':ALL}, 'n_clicks'),
    prevent_initial_call=True
)
def print_inputs(buttons):
    return str(ctx.inputs_list)

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

Workaround (it only works if the broader/higher-level category has a pre-defined number of items)

@callback(
    Output('out', 'children'),
    [Input({'type': 'button', 'card_number': card_idx, 'index':ALL}, 'n_clicks') for card_idx in range(1,4)],
    prevent_initial_call=True
)
def print_inputs(buttons_c1, buttons_c2, buttons_c3):
    return str(ctx.inputs_list)

Workaround variation with flexible callback signatures:

import dash
from dash import Dash, dcc, html, Input, Output, State, callback
from dash import ALL, MATCH, Patch, ctx

app = Dash(__name__)

app.layout = html.Div([
    html.Div(
        id={'type': 'card', 'card_number': card_idx},
        children = [
            html.Button(
                id={'type': 'add', 'card_number': card_idx},
                children='Add new button'
            ),
            html.Button(
                id={'type': 'button', 'card_number': card_idx, 'index': 0},
                children=f"card{card_idx}_button0"
            )
    ]) for card_idx in range(1,4)
] + [
    html.Button(id='lonely_button', children='lonely button'),
    html.Div(id='out', children='No button clicked yet')
])

@callback(
    Output({'type': 'card', 'card_number': MATCH}, 'children'),
    Input({'type': 'add', 'card_number': MATCH}, 'n_clicks'),
    prevent_initial_call=True
)
def add_button_to_card(n_clicks):
    card_idx = ctx.triggered_id['card_number']
    card_children = Patch()
    card_children.append(
        html.Button(
                id={'type': 'button', 'card_number': card_idx, 'index': n_clicks},
                children=f"card{card_idx}_button0"
            )
    )
    return card_children

@callback(
    Output('out', 'children'),
    inputs=dict(
        grouped_buttons=[Input({'type': 'button', 'card_number': card_idx, 'index':ALL}, 'n_clicks') for card_idx in range(1,4)],
        lonely_button=Input('lonely_button', 'n_clicks')
    ),
    prevent_initial_call=True
)
def print_inputs(lonely_button, grouped_buttons):
    return str(ctx.inputs_list)

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

Additional context

dash==2.14.1
@AnnMarieW
Copy link
Contributor

AnnMarieW commented Apr 19, 2024

@celia-lm

Would the ctx.args_grouping be helpful?

See more info in the args_grouping examples in this forum post

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

2 participants