Hey all,
I went a bit wild with creating pattern matching dynamic elements.
My UI has the following:
- Multiple dbc.Tabs
- Each tabs has multiple dbc.Tables with 10-500 cells each
- Most of the cells are html.As with a pattern matching id, identifying the cell type (column) and a certain other id (row).
- I have a callback that listens to clicks on these html.As. Based on the type and id it generates a modal showing some plotly chart unique for this type and id combination.
So far sounds ok I hope. Where this gets complicated:
- When the user plays with other elements in the GUI the tables are regenerated. Some columns and rows appear and disappear, and some cells stop being html.As and become simple (âNAâ) text, or the other way around.
- The tabs may show the same elements. So tab 1 may show an html.A with the same type and id as shown in tab 2.
Trivially, I started with using this input:
Input({âtypeâ: MY_TYPE, âcell_typeâ: ALL, âcell_idâ: ALL, ân_clicksâ)
However, this caused many false triggers whenever cells appeared, disappeared or reappeared. In my case - false trigger = popup appearing unnecessarily. Not a good behavior.
Anyway, after a lot of tracing of each and every such case, I managed to code the below monstrosity that avoids 99.9% of the false triggers.
Clearly Iâm doing something wrong⌠Beyond a bunch of ifs, I resorted to storing the n_clicks array in a dcc.store⌠Any ideas?
@callback(
...outputs...
Output(STORE_MODAL_NCLICKS_ID, 'data'),
Input({'type': MY_TYPE, 'cell_type': ALL, 'cell_id': ALL, 'n_clicks')
State(STORE_MODAL_NCLICKS_ID, 'data'),
...states...
prevent_initial_call=True
)
def show_popup_chart(n_clicks, n_clicks_store, ...):
empty_return = X * dash.no_update, n_clicks
trigger = ctx.triggered_id
if trigger is None: # triggers happen on cell DISappearance
return empty_return
# if this is not a normal click, then avoid the value comparison false alarm case.
# this is needed since there are cases where dash.callback_context.triggered is 0 even though there was a click.
# happens since an element of the same name is sometimes created in both tabs, which messes stuff up
skip_val_compare = False
if (n_clicks is not None and n_clicks_store is not None and len(n_clicks) == len(n_clicks_store) and
sum(a - b == 1 for a, b in zip(n_clicks, n_clicks_store) if a is not None and b is not None) == 1):
skip_val_compare = True
# avoids trigger on initial creation of tables and some actions which create new elements in n_clicks
if all(d['value'] == 0 or d['value'] is None for d in dash.callback_context.triggered) and not skip_val_compare:
return empty_return
# X element movements cause a trigger but don't change n_clicks or set some (not all) of n_clicks elements to 0
# except when new elements are added to n_clicks, then we still want a trigger. this condition should be last
if (n_clicks_store is not None
and all(a == b or (a == 0 and b != 0) for a, b in zip(n_clicks, n_clicks_store))
and len(n_clicks) <= len(n_clicks_store)):
return empty_return