Learn how to use Dash Bio for next-gen sequencing & quality control. 🧬Register for the Oct 27 webinar.

Trouble linking dbc Tooltips to pattern matched objects

I am developing a dashboard and decided to create a few builder functions to produce card containers along with objects in them. The metrics need additional context at times so I add a hover image which has a tooltip that says “Click for more details” and when clicked, it brings up a unique modal. I got the pattern matching callback working for the modals but having a dict as a tooltip target doesn’t work and I can’t think of a workaround. It throws the following errors.

Screen Shot 2020-06-20 at 7.29.22 AM

TypeError: r.removeEventListener is not a function

    at http://127.0.0.1:8888/_dash-component-suites/dash_bootstrap_components/_components/dash_bootstrap_components.v0_9_2m1586518166.min.js:31:204395

    at Array.forEach (<anonymous>)

    at t.n.removeEventOnTargets (http://127.0.0.1:8888/_dash-component-suites/dash_bootstrap_components/_components/dash_bootstrap_components.v0_9_2m1586518166.min.js:31:204372)

    at t.n.removeTargetEvents (http://127.0.0.1:8888/_dash-component-suites/dash_bootstrap_components/_components/dash_bootstrap_components.v0_9_2m1586518166.min.js:31:205063)

    at t.n.componentWillUnmount (http://127.0.0.1:8888/_dash-component-suites/dash_bootstrap_components/_components/dash_bootstrap_components.v0_9_2m1586518166.min.js:31:201996)

    at callComponentWillUnmountWithTimer (http://127.0.0.1:8888/_dash-component-suites/dash_renderer/react-dom@16.v1_4_1m1592401879.13.0.js:19748:14)

    at HTMLUnknownElement.callCallback (http://127.0.0.1:8888/_dash-component-suites/dash_renderer/react-dom@16.v1_4_1m1592401879.13.0.js:182:16)

    at Object.invokeGuardedCallbackDev (http://127.0.0.1:8888/_dash-component-suites/dash_renderer/react-dom@16.v1_4_1m1592401879.13.0.js:231:18)

    at invokeGuardedCallback (http://127.0.0.1:8888/_dash-component-suites/dash_renderer/react-dom@16.v1_4_1m1592401879.13.0.js:286:33)

    at safelyCallComponentWillUnmount (http://127.0.0.1:8888/_dash-component-suites/dash_renderer/react-dom@16.v1_4_1m1592401879.13.0.js:19755:7)
(This error originated from the built-in JavaScript code that runs Dash apps. Click to see the full stack trace or open your browser's console.)
TypeError: r.addEventListener is not a function

    at http://127.0.0.1:8888/_dash-component-suites/dash_bootstrap_components/_components/dash_bootstrap_components.v0_9_2m1586518166.min.js:31:204291

    at Array.forEach (<anonymous>)

    at t.n.addEventOnTargets (http://127.0.0.1:8888/_dash-component-suites/dash_bootstrap_components/_components/dash_bootstrap_components.v0_9_2m1586518166.min.js:31:204268)

    at t.n.addTargetEvents (http://127.0.0.1:8888/_dash-component-suites/dash_bootstrap_components/_components/dash_bootstrap_components.v0_9_2m1586518166.min.js:31:204720)

    at t.n.updateTarget (http://127.0.0.1:8888/_dash-component-suites/dash_bootstrap_components/_components/dash_bootstrap_components.v0_9_2m1586518166.min.js:31:205617)

    at t.n.componentDidMount (http://127.0.0.1:8888/_dash-component-suites/dash_bootstrap_components/_components/dash_bootstrap_components.v0_9_2m1586518166.min.js:31:201922)

    at commitLifeCycles (http://127.0.0.1:8888/_dash-component-suites/dash_renderer/react-dom@16.v1_4_1m1592401879.13.0.js:19982:24)

    at commitLayoutEffects (http://127.0.0.1:8888/_dash-component-suites/dash_renderer/react-dom@16.v1_4_1m1592401879.13.0.js:22969:9)

    at HTMLUnknownElement.callCallback (http://127.0.0.1:8888/_dash-component-suites/dash_renderer/react-dom@16.v1_4_1m1592401879.13.0.js:182:16)

    at Object.invokeGuardedCallbackDev (http://127.0.0.1:8888/_dash-component-suites/dash_renderer/react-dom@16.v1_4_1m1592401879.13.0.js:231:18)
def info_img_template(id):
    return html.Img(
        src="/assets/info_icon.png",
        height="18px",
        className="info_icon",
        id={'type': 'dynamic-info_img',
            'index': id},
        n_clicks=0
    )


def tooltip_template(target):
    return dbc.Tooltip(
        "Click for more details",
        target={'type': 'dynamic-info_img',
        'index': target},
        placement="bottom"
    )


def modal_template(index, data):
    return dbc.Modal([
        dbc.ModalHeader(data['title']),
        dbc.ModalBody(data['body']),
        dbc.ModalFooter(
            dbc.Button(
                "Close",
                id={
                    'type': 'dynamic-close',
                    'index': index
                },
                className="ml-auto")
        )],
        id={
            'type': 'dynamic-modal',
            'index': index
    })


def build_cards(card_data):
    card_dict = {'card': {}, 'tooltip': {}, 'modal': {}}
    for index, data in enumerate(card_data):
        if 'graph_id' in data:
            salt = data['graph_id']
        else:
            salt = index
        if 'info' in data:
            img = info_img_template(salt)
            data['info_img'] = img
            card_dict['tooltip'][f'tooltip_{salt}'] = tooltip_template(salt)
            card_dict['modal'][f'modal_{salt}'] = modal_template(salt, data['info'])

        card_dict['card'][f'card_{salt}'] = create_card(
            **data  # Will pass along the dict of variables
        )
    return card_dict

# Patten Matching callbacks for modals
@app.callback(
    Output({'type': 'dynamic-modal', 'index': MATCH}, 'is_open'),
    [Input({'type': 'dynamic-info_img', 'index': MATCH}, 'n_clicks'),
     Input({'type': 'dynamic-close', 'index': MATCH}, 'n_clicks')],
    [State({'type': 'dynamic-modal', 'index': MATCH}, 'is_open')],
)
def display_output(n1, n2, is_open):
    if n1 or n2:
        return not is_open
    return is_open

dash_table version 4.7.0
Plotly version 4.6.0
dcc version 1.10.0
html version 1.0.3
dbc version 0.10.2

1 Like

I have exactly the same problem. Instead of tooltips, I am using the dbc.Popover. When I’m triggering it with a normal callback it works fine, but if I use the pattern matching method I get the same error as @bphillip:
r.addEventListener is not a function and r.removeEventListener is not a function

If anyone knows a workaround I would be happy for your support.

Hey both,

The underlying reactstrap implementation of the tooltip uses document.querySelector to find the element by id and attach the tooltip to it. When you used dictionary ids, Dash serializes the dictionary as JSON then uses that for the id. While this is a valid HTML id, it is not a valid CSS selector, which means querySelector fails, so unfortunately we can’t support dictionary IDs with tooltip currently.

There is however a pattern that you can use to work around this in some cases, which is to add a wrapper with a controlled id to the element when you create it. Something like

html.Div(
    [
        # wrapper with controlled id
        html.Div(
            html.Div(id={"type": "div", "index": n}),
            id=f"tooltip-div-wrapper-{n}",
        ),
        dbc.Tooltip(
            f"The {n}th tooltip", target=f"tooltip-div-wrapper-{n}"
        ),
    ],
)
4 Likes

Thanks so much, @tcbegley, worked for me :muscle:. There is a typo in dbc.ToolTip which should be `dbc.Tooltip. Just in case someone is struggling with running it :slight_smile:

For anyone that needs an example for the popover:

html.Div(
    [
        html.Div(
            dbc.Button(
                "Click to toggle popover", id={
                    'type': 'popover-target',
                    'index': id
                }, color="danger"
            ),
            id=f"popover-div-wrapper-{id}"
        ),
        dbc.Popover(
            [
                dbc.PopoverHeader("Popover header"),
                dbc.PopoverBody("And here's some amazing content. Cool!"),
            ],
            id={
                'type': 'popover',
                'index': id
            },
            is_open=False,
            target=f"popover-div-wrapper-{id}"
        ),
    ]
)

And for the callback I’m using is this one:

@app.callback(
    Output({'type': 'popover', 'index': MATCH}, "is_open"),
    [Input({'type': 'popover-target', 'index': MATCH}, "n_clicks")],
    [State({'type': 'popover', 'index': MATCH}, "is_open")],
)
def toggle_popover(n, is_open):
    if n:
        return not is_open
    return is_open
2 Likes