Pattern matching callbacks - Input(MATCH), Output(not MATCH)

I want to use Dash pattern matching callbacks’ MATCH as an Input, without needing to Output to the same MATCH. Imagine I generate a long, random list of items, from which a user may select any number of items. As they select each item, I want to append that item to a list in real time. It’s a long list of 1,000+ items, so I don’t want to see the whole ALL list, just the one they’ve selected in real time, so I can do a fast, efficient callback.

Something like the following:

from dash import Dash, dcc, html, Input, Output, State, MATCH, ALL
import dash_bootstrap_components as dbc

app = Dash(__name__, suppress_callback_exceptions=True)

# HTML table header and rows
header = html.Thead(html.Tr([html.Th("Add Part"), html.Th("Description")]))
rows = html.Tbody(
    [
        html.Tr(
            [
                html.Td(
                    dbc.Button(
                        "Add",
                        id={"type": "part_add", "index": i},
                        n_clicks=0,
                    ),
                ),
                html.Td(f"Index {i}"),
            ]
        )
        for i in range(1000)
    ]
)

app.layout = html.Div([
    dcc.Store(id="my_list", storage_type="local", data=[]),
    dbc.Table(
        [header, rows],
        bordered=True,
        striped=True,
    )
])


@app.callback(
    Output("my_list", "data"),
    Input({"type": "part_add", "index": MATCH}, "id"),
    State("my_list", "data"),
)
def match_callback_test(
    part_add_id,
    my_list_data 
):
    return my_list_data.append(part_add_id)

Is the above possible, with a different pattern? I get a JavaScript console error if I run the above.

Hi @seanrez !

This is a very interesting use case and I’m trying to find the best solution in terms of performance. Your MRE was really helpful for that, thank you!

In the meantime, I wanted to explain why using MATCH in an Input without MATCH in the Output does NOT work: As you probably already no, Dash does not allow duplicated outputs. That is, you cannot have more than one callback that uses the same component_property+component_id.

Pattern matching callbacks with MATCH are equivalent to generating one callback for each of the components with that type of id. That is:

@app.callback(
    Output("my_list", "data"),
    Input({"type": "part_add", "index": MATCH}, "id")
)

Would be equivalent to writing:

@app.callback(
    Output("my_list", "data"),
    Input({"type": "part_add", "index": 1}, "id")
)
...

@app.callback(
    Output("my_list", "data"),
    Input({"type": "part_add", "index": 2}, "id")
)
...

@app.callback(
    Output("my_list", "data"),
    Input({"type": "part_add", "index": 3}, "id")
)
...

(etc)

In consequence, we would generate the problem of duplicated outputs.

I hope this helps to clarify the issue a bit.

One question about your problem: how do you display the 1000 buttons? I assume not all at the same time. Maybe you have a search bar, pagination of scrolling? Rendering the buttons in smaller batches (e.g. 20 at a time) might help with the performance issues while using the pattern-matching selector ALL.

1 Like

Hi again. A possible workaround would be, instead of using dbc.Table with buttons, using a dash_table and listening to the active cell in a callback.

1 Like

Hi Celia,

Thanks a lot for the explanation and brainstorming. Here’s how we display the “parts” to the user, in a big HTML table inside a Bootstrap modal. It’s searchable, and quite popular, but the users want to be able to “add the parts” as they search for them.

I was actually thinking about doing a large Bootstrap radio input, since the labels can have their own IDs, and I was thinking I’d somehow hide the items using their label IDs, but then I’m right back to using MATCH and ALL again…

I also like your idea of using a dash_table instead. Actually that’s probably the best idea. In fact, I’m actually using this “parts lookup” modal to fill out dash_table rows, as chance would have it!

I still think there should be a way of using MATCH in just the inputs, and not needing it in the Output as well, or at least allowing a person to have multiple outputs, some of which are not MATCH. Then I could just return dash.no_update for the MATCH output I don’t actually want to update, and instead I could update the other output. Is that possible?

Thanks again,
Sean

@seanrez
Oh, I see what you are trying to do. I think the easiest way is to use a DataTable. You could use the built-in filters in the part number and description columns. You also could make the rows selectable, then include an “add to order” button in the Parts Lookup table. This would be much easier - and faster than using pattern matching callbacks.

1 Like

I would like to revive this topic in light of the new “allow_duplicate” argument for Output that was added in version 2.9, specifically because of @celia 's explanation on why it’s not possible to use MATCH in an Input without MATCH in the Output.

Now that you can have multiple callbacks with the same Output, wouldn’t this be possible? I know that the specific use case of the creator of the question was solved (through other means); but the original question remains relevant. Being able to use MATCH only in the Input would be a great feature to have!

1 Like

HI Wilfredo! Actually, there’s already an open feature request in the Dash repo to remove this limitation: [Feature Request] Remove Callback Wildcard Restrictions (MATCH) · Issue #2462 · plotly/dash · GitHub

3 Likes

For those looking for alternatives to the ALL and MATCH limitations, here’s how I turned a dbc.RadioItems element into a long list with “Add” buttons instead of radio buttons. It’s very efficient/fast and accomplishes exactly what I needed.

My CSS to change radio buttons to add buttons:

/* Hide the original radio button to the left of the label */
.custom-radio-button {
  border: 0 !important;
  clip: rect(1px, 1px, 1px, 1px);
  height: 1px !important;
  overflow: hidden;
  padding: 0 !important;
  position: absolute !important;
  width: 1px !important;
}

.custom-radio-label {
  margin-top: 0.3rem;
  /* display: block; */
  display: inline-flex;
  align-items: center;
}

/* The new button */
.custom-radio-label::before {
  content: "Add";
  /* display: block; */
  vertical-align: middle;
  margin-right: 0.5rem;
  padding: 0.25rem 0.5rem;
  font-size: 0.875rem;
  border-radius: 0.2rem;
  border: 1px solid #007bff;
  /*   color: #007bff; */
  /* to show the pointer on hover */
  cursor: pointer;
  /* smooth transition */
  transition: background-color 0.3s, color 0.3s;
  /* transition: border ease-in 150ms, box-shadow ease-in 150ms; */
}

/* When you hover over the button to the left of the label */
.custom-radio-label:hover::before {
  background-color: #007bff;
  color: #ffffff;
}

/* Add a box-shadow after it's clicked */
.custom-radio-button:focus + .custom-radio-label::before {
  box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.5);
}

and the Python code for the RadioItems:


                                dbc.RadioItems(
                                    id="work_order_parts_lookup_radio",
                                    # Special classes so the radio items look like Bootstrap buttons
                                    # input_checked_class_name="btn-primary",
                                    # input_checked_style={"color": "white"},
                                    input_class_name="custom-radio-button",
                                    # input_style={"color": "black"},
                                    # label_checked_class_name = "btn-primary",
                                    # label_checked_style = {"color": "white"},
                                    label_class_name="custom-radio-label",
                                    # label_style = {"color": "black"},
                                    # style={"ps": "0"},
                                    class_name="ps-0",
                                )