Pseudo-dynamic callbacks / workaround

Hi

I have found a workaround for not having dynamic callbacks which still allows adding new elements and new callbacks dynamically:

My (simplified) app works as follows:

  • I want to render a form with a variable number of questions / inputs.
    • This number of questions / inputs is usually known in advance => This can be perfectly handled by dash.
  • Now, I want to add a button after each Question “Insert a new question here” such that when I am clicking on it, a new question / input with appropriate callback is added to the form.

But: dynamic callbacks are not supported

In my app, the operation “insert new question here” is relatively rarely called. So I could accept a page reload for the few cases when this happens. So I can (ab-)use a dcc.Location with refresh=True to trigger page reloads.

Thanks to new persistence function, the inputs keep their value even after a page reload(!).

So every “insert new” button is an Input for the dcc.Location. In this callback, I add the new question to the layout and build a new callback dynamically. The callback for the new field is then also working (Yeah!).

The biggest issue was, how to add the new button as an input to the dcc.Location. I found that I can directly edit the app.callback_map for this purpose.

The following code demonstrates the whole workings:

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


external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.config.suppress_callback_exceptions = True


QUESTIONS = [0, 1]  # Global variabel only for demonstration purposes


def layout():
    layout = html.Div([
        dcc.Location(id='reload-location', refresh=True),
        html.Div([
            html.Div([
                html.Div([
                    html.Label(f'Question {i}', htmlFor=f'input-{i}'),
                    dcc.Input(id=f'input-{i}', type='text', persistence=True)
                ]),
                html.Div('Nothing yet', id=f'output-{i}'),
                html.Button('Insert new question here', id=f'button-{i}')
            ]) for i in QUESTIONS],
            id='questions')
    ])
    return layout


app.layout = layout  # importantly just use the function  and not the call


def make_single_callback(i):
    @app.callback(Output(f'output-{i}', 'children'),
                  [Input(f'input-{i}', 'value')])
    def process(value):
        if value is None:
            return no_update
        # Do the processing here
        # ...
        # in the example this is OK
        return f'You typed {value}'


def make_all_callbacks(questions):
    @app.callback(Output('reload-location', 'href'),
                  [Input(f'button-{i}', 'n_clicks') for i in questions])
    def process(*args):
        if all([a is None for a in args]):  # Startup
            return no_update

        ctx = dash.callback_context
        trigger = ctx.triggered[0]
        button_number = int(re.search(r'\d+', trigger['prop_id']).group())
        new_i = len(QUESTIONS)
        QUESTIONS.insert(button_number + 1, new_i)

        app.callback_map['reload-location.href']['inputs'].append({'id': f'button-{new_i}', 'property': 'n_clicks'})

        print(app.callback_map)
        make_single_callback(new_i)

        return f'/{trigger["prop_id"]}/{trigger["value"]}'  # does it matter?

    for i in questions:
        make_single_callback(i)


make_all_callbacks(QUESTIONS)


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


Edit: Typo

1 Like