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