Deletion of last row containing delete-button which is target of dbc.Tooltip leads to invalid children output
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 24.4k
- Forks
- 2.3k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 13
Description
Describe your context
dash_bootstrap_components==1.2.1
dash==2.7.0
Describe the bug
I have implemented a simple editor, which enables you to add and delete rows using a pattern-matching-callback. Each row contains a delete-button, which is target of a dbc.Tooltip.

When deleting the very last row, an error occurs when returning adjusted children for div selection-area:
An object was provided as `children` instead of a component, string, or number (or list of those). Check the children property that looks something like:
{
"props": {
"children": [
{
"props": {
"children": [
null,
{
"props": {
"is_open": false
}
}
]
}
}
]
}
}
- According to the console prints in my code below, the callback is unexpectedly called twice by the delete-button-pattern-input when clicking delete on the very last row.
- The second time the callback is called, the last element of the input
selection_elementsis the object from the above error.
- The second time the callback is called, the last element of the input
- If we set a breakpoint on the return command at the end of the callback or apply
time.sleep(1), the bug doesn't occur. - The bug doesn't occur when deleting rows from the middle or top.
- The bug doesn't occur if we don't use the delete-button as target of the
dbc.Tooltip.
Full runnable code for investigation of the bug:
import dash
from dash import Dash, dcc, html
from dash.exceptions import PreventUpdate
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State, ALL
import json
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
def make_input(i):
return dbc.Row([dbc.Col(
dcc.Input(id={"index": i, "type": "selection-input"},
type='text',
value='',
placeholder='e.g. c1-c0',
className='w-100'), lg=9)])
def make_input_row(i):
elements = (dbc.Col([html.Div(dbc.Button(children=['Delete'], id={"index": i, "type": 'remove-row-button'}), id=f"remove-row-button-tooltip-wrapper-{i}"),
dbc.Tooltip(
'Delete Row',
target=f"remove-row-button-tooltip-wrapper-{i}",
placement='left')
], lg=1),)
elements = elements + (dbc.Col(make_input(i), lg=4),)
return elements
app.layout = html.Div([html.Div(id='selection-area', children=[
dbc.Row(children=[
*make_input_row(0)
], id={"index": 0, "type": "selection-area-row"}, className="mt-2"),
dbc.Row(children=[
*make_input_row(1)
], id={"index": 1, "type": "selection-area-row"}, className="mt-2"),
dbc.Row(children=[
*make_input_row(2)
], id={"index": 2, "type": "selection-area-row"}, className="mt-2"),
dbc.Row(children=[
*make_input_row(3)
], id={"index": 3, "type": "selection-area-row"}, className="mt-2")
]), dbc.Row(children=[
dbc.Col(lg=1),
dbc.Col(dbc.Button('Add', id='add-row-button', outline=True, color='primary', style={'width': '100%'}), lg=3)
], className="mt-2"), ])
def edit_selection_area(remove_click, selection_elements):
trigger_id = dash.callback_context.triggered[0]['prop_id'].split('.')[0]
if trigger_id == 'add-row-button' and len(selection_elements) < 10:
i = max([channel_element['props']['id']['index'] for channel_element in selection_elements]) + 1 if selection_elements else 0
selection_elements.append(dbc.Row(children=[
*make_input_row(i)
], id={"index": i, "type": "selection-area-row"}, className="mt-2"))
elif 'remove-row-button' in trigger_id and len(selection_elements) > 1 and any(remove_click):
i = json.loads(trigger_id)['index']
selection_elements = [selection_element for selection_element in selection_elements if selection_element['props']['id']['index'] != i]
return selection_elements
@app.callback(
Output('selection-area', 'children'),
[Input('add-row-button', 'n_clicks'),
Input({'type': 'remove-row-button', 'index': ALL}, 'n_clicks')],
[State('selection-area', 'children')]
)
def edit_selection_area_callback(add_click, remove_click, selection_elements):
trigger_id = dash.callback_context.triggered[0]['prop_id']
if "remove-row-button" in trigger_id and not any(remove_click):
print("TRIGGERED A SECOND TIME WHEN CLICKING THE LAST ELEMENT:")
print("trigger_id: ", trigger_id)
print("remove_click: ", remove_click)
print(f"input selection elements: {len(selection_elements)} ", selection_elements)
raise PreventUpdate
print("trigger_id: ", trigger_id)
print("remove_click: ", remove_click)
print(f"input PreventUpdate elements: {len(selection_elements)} ", selection_elements)
children = edit_selection_area(remove_click, selection_elements)
print(f"output PreventUpdate elements: {len(children)} ", children)
print()
return children
if __name__ == "__main__":
app.run_server(debug=True)
Expected behavior
Deletion of the last row containing a delete-button which is target of a dbc.Tooltip should behave the same way as deletion of rows from the middle or top.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the runnable reproduction in the issue using dash 2.7.0 and dash_bootstrap_components 1.2.1, then inspect edit_selection_area_callback and its pattern-matching remove-button callback. Done means deleting the final row with a dbc.Tooltip does not trigger the invalid children output and behaves like deleting a middle or top row.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100