plotly / plotly/dash

Deletion of last row containing delete-button which is target of dbc.Tooltip leads to invalid children output

Open
#2,311 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug dash-data-table P3
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.

image

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_elements is the object from the above error.
  • 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.