Pseudo-dynamic callbacks / workaround


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 = ['']
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.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],
    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('\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'})


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

    for i in questions:


if __name__ == '__main__':

Edit: Typo

1 Like