Is it possible to assign a callback to a button generated by a function call?

Code redundancy is the root of all evil in software development, so I try to not repeat myself and write a function for frequently used UI components of my Dash app. Take a look at this search field and button:

def search_field(field_id, button_id, label, placeholder=""):
    field = html.Div(
        className="field has-addons",
        children=[
            html.Label(label, className="label")
            html.Div(
                dcc.Input(
                    id=field_id,
                    className="input",
                    type="text",
                    placeholder=placeholder
                ),
                className="control"
            ),
            html.Div(
                html.Button(
                    html.I(className="fas fa-search"),
                    id=button_id,
                    className="button"
                ),
                className="control"
            )
        ]
    )
    return field

I use a function call to insert such a button into my layout:

elements.search_field(
    field_id="search-term-field",
    button_id="search-button",
    label="Beschreibungen durchsuchen",
    placeholder="Suchbegriff"
)

So far so good:

52

However, assigning a callback to the button does not seem to work:

@app.callback(
    Output('search-result-area', 'children'),
    [
        Input('search-button', 'n_clicks')
    ],
    [
        State('search-term-field', 'value')
    ]
)
def update_search_result(n_clicks, search_term):
    ...

Is this something that Dash does not support?

Is the button generated before or after the app is started?

The function call that generates it happens before the app is started.

All your callbacks need to be registered before the app starts, which it sounds like you’re doing. The issue is probably that you’re modifying the DOM directly. This generally doesn’t play well with React, which has it’s own virtual DOM, and there’s also a bunch of logic that the Dash renderer does to keep track of modifications to the layout, which won’t be triggered via direct access.

In general, I think you need to restrict layout changes to being performed by Dash callbacks.

I’d like to understand better what you mean by “modifying the DOM directly”.

In general, I think you need to restrict layout changes to being performed by Dash callbacks.

I’m also not sure that I understand this correctly. I would say the layout changes are ultimately all performed by callbacks. I use function calls to make the code that constructs the layout less verbose and redundant. From a Python point of view, this code should be equivalent.

If the callbacks are attached prior to starting the app, I see no reason that it should not work. Can you post a MWE?

Oh right, sorry. I thought that second snippet was JavaScript for some reason, so I guess ignore my comments about modifying the DOM directly.

It sounds like what you’re trying to do should work. Echoing @Emil, can you post a minimal working example that shows this issue?

Here is a fairly minimal example that is not working as intended. I am trying to show a new box in a previously empty area on the click of a button. The button is generated via a function call.

(I think components.text_field illustrates why it is necessary to wrap frequently used parts of the layout into a function: Typing this out for every field of a form would quickly amount to unreadable code)

When clicked, the button just doesn’t do anything, and there is no error message.

Content of main.py:

import flask

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State

import components

# linked resources
## stylesheets
stylesheets = [
    "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css", # Bulma
]


# app
## flask server
server = flask.Flask(__name__)

## create app
app = dash.Dash(
    __name__,
    server=server,
    external_stylesheets=stylesheets
)


# layout

## navigation
navbar = html.Nav(
    className="navbar",
    children=[
        html.Div(
            className="navbar-brand",
            children=[
                html.Div(
                    className="navbar-item",
                    children=[
                    ]
                )
            ]
        ),
        html.Div(
            className="navbar-menu is-active",
            children=[
                html.Div(
                    className="navbar-start",
                    children=[]
                )
            ]
        ),
        html.Div(
            className="navbar-end",
            children=[
                dcc.Link("Support", className="navbar-item"),
            ]
        )
    ]
)


## boxes
sim_param_box = html.Div(
    id="sim-param-area",
    className="box",
    children=[
        components.text_field(
            field_id="start-time-field",
            label="Start",
            placeholder="YYYY/MM/DD hh:mm"
        ),
        components.text_field(
            field_id="stop-field",
            label="Stop",
            placeholder="YYYY/MM/DD hh:mm"
        ),
        components.text_field(
            field_id="tick-field",
            label="1 Tick =",
            placeholder="[s]"
        ),
        components.enter_button(
            button_id="sim-param-enter-button",
            text=None,
            tooltip="Simulation initialisieren"
        )
    ]
)

sim_param_area = html.Div(
    children=sim_param_box,
    className="section",
)

sim_results_box = html.Div(
    id="sim-results-box",
    className="box",
    children=[
        "Test"
    ]
)

sim_results_area = html.Div(
    id="sim-results-area",
    className="section",
    children=[],
)


## main layout

main_area = html.Div(
    className="content",
    id="main-area",
    children=[
        sim_param_area,
        sim_results_area
    ]
)

app.layout = html.Div(
    className="container",
    children=[
        navbar,
        main_area
    ]
)


# callbacks

@app.callback(
    Output("sim-results-area", "children"),
    [
        Input("sim-param-enter-button", "n_clicks")
    ]
)
def enter_sim_params(n_clicks):
    if n_clicks:
        return sim_results_box

if __name__ == '__main__':
    app.run_server(debug=True)

Content of components.py:

import dash
import dash_core_components as dcc
import dash_html_components as html

def text_field(field_id, label=None, placeholder=None):

    field = html.Div(
        children=[
            html.Label(label, className="label"),
            html.Div(
                dcc.Input(
                    className="input",
                    type="text",
                    placeholder=placeholder,
                ),
                id=field_id,
                className="control"
            )
        ],
        className="field",
    )
    return field


def enter_button(button_id, text=None, tooltip=""):
    button_label = html.I("⏎")
    if text:
        button_label = text
    return html.Button(
        id=button_id,
        title=tooltip,
        className="button is-large is-primary is-outlined",
        style={"width": "250px"},
        children=[
            button_label
        ]
    )

Sorry for the slow reply. Just tried out your example, but I’m not sure I can see the issue you’re describing. When I click the enter button with the arrow on it, a box with “Test” in it is created successfully. Is that the behaviour you’re saying is not happening?

Perhaps try updating you all your Dash libraries to the latest versions.

Oh, and the answer to your question is yep, you can definitely use functions to wrap up and abstract away layout creation. It’s a great way to make your apps more reusabale and reusable.

Thanks for confirming that it works for you. Will check whether the issue has disappeared after a restart, as has been the case before.

It seems that one thing that makes Dash apps very difficult to debug when you generate components with function calls is that you need to suppress exceptions:

app.config['suppress_callback_exceptions'] = True

In effect, some things just don’t work and you don’t get error messages. Trial and error debugging follows. Could this be improved?

It is actually possible to retain callback validation, however you have to do some work. See the section “Dynamically Create a Layout for Multi-Page App Validation” in the Multi Apps page of the Dash docs and the code snippet following.

You essentially need to create a layout function that checks whether Dash is returning the layout for a request, or just for validation. If for validation, then the function you make needs to return every possible layout component all concatenated together, and you’ll get them validated.

The catch is that you can’t combine callbacks and layouts within the different modules for each page when doing this. See this issue: https://github.com/plotly/dash/issues/519

1 Like