Callback parameter is created inside another callback

Hi,

I’m currently creating an app that requires a dynamic change in the Input parameter for a callback depending on another callback. Here is my example. The first callback will create/update the global variable special which is used in the second callback. The length of special is changing. However, one you select ‘A’, the second callback is no longer able to receive the special parameter. So args becomes empty. Any idea on how to solve this. Btw, I’m new to Dash and my coding style may not be very ideal. Thanks for your help!

import dash
from dash.dependencies import Input, Output # for user interface
import dash_core_components as dcc
import dash_html_components as html


app = dash.Dash(__name__)
app.layout = html.Div(children=[
    html.Div(dcc.Dropdown(id='select_input', options=[{'label': 'A', 'value': 'A'}, {'label': 'B', 'value': 'B'}])),
    html.Div([dcc.Input(id='text_input', type='text')]),
    html.Div(id='output1'),
    html.Div(id='output2'),
])


@app.callback(
    Output('output1', 'children'),
    [Input('select_input', 'value')],
)
def dropdown_options(select_input):
    if select_input is None:
        return html.P('please select')
    if select_input == 'A':
        global special
        special =  [Input('select_input', 'value'), Input('text_input',  'value')]
        return html.P(f'special is updated!')
    else:
        return html.P('special will not be updated.')

special = [Input('select_input', 'value')]

@app.callback(
    Output('output2', 'children'),
    special
)
def uploaded_files(option, *args):
    if option == 'A' and len(args) == 0:
        return html.P(f'You selected A, the length of special is: {len(special)}, and length of args is: {len(args)}')
    if option == 'A' and len(args) != 0:
        return html.P(f'You selected A and entered {args}')
    if option == 'B':
        return html.P(f'you have selected B, text will be ignored!')
    return  html.P(f'Before u select, the length of special is: {len(special)}, and length of args is: {len(args)}')


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

This won’t work. The output, inputs, and state arguments of a callback are used to build the webpage when run_server is called. After this point you may be able to change the Python code but it will break the interaction between the web page and the Python.

I have rewritten your code in a way that does not require dynamically changing the callback. It uses the recently announced dash.callback_context.triggered from 📣 Dash 0.38.0 released to figure out which input triggered the callback and respond appropriately.

import dash
from dash.dependencies import Input, Output # for user interface
import dash_core_components as dcc
import dash_html_components as html


app = dash.Dash(__name__)
app.layout = html.Div(children=[
    html.Div(dcc.Dropdown(id='select_input', options=[{'label': 'A', 'value': 'A'}, {'label': 'B', 'value': 'B'}])),
    html.Div([dcc.Input(id='text_input', type='text')]),
    html.Div(id='output1'),
    html.Div(id='output2'),
])


@app.callback(
    Output('output1', 'children'),
    [Input('select_input', 'value')],
)
def dropdown_options(select_input):
    if select_input is None:
        return html.P('please select')
    if select_input == 'A':
        return html.P(f'special is updated!')
    else:
        return html.P('special will not be updated.')


@app.callback(
    Output('output2', 'children'),
    [Input('select_input', 'value'), Input('text_input',  'value')]
)
def uploaded_files(option, special):
    if option is None:
        raise dash.exceptions.PreventUpdate
    if special is None:
        special = ''

    triggered_input = dash.callback_context.triggered[0]['prop_id']

    if option == 'A' and triggered_input == 'select_input.value':
        return html.P(f'You selected A, the length of text input is: {len(special)}')
    if option == 'A' and triggered_input == 'text_input.value':
        return html.P(f'You selected A and entered {special}')
    if option == 'B':
        return html.P(f'you have selected B, text will be ignored!')


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

It’s not exactly the behavior you describe but hopefully you should be able to get this approach to work as you want.

1 Like

Hi Damian, thanks so much for answering my question so quickly. That, um…, not really solves my problem. Here I simplified my code further so you can see the problem more clearly. I don’t know how to code STATE part so the length of it can change based on the drop down selection. In my first question, I was asking how to generate this STATE part inside a callback function coz I think it might get updated but I failed. It might look rather simple but hard for me to solve it. Thanks again for your help.

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

app = dash.Dash()
app.config['suppress_callback_exceptions']=True
app.layout = html.Div(children=[
    html.Div(dcc.Dropdown(id='select_input',
                          options=[{'label': '2 inputs', 'value': '2'}, {'label': '3 inputs', 'value': '3'}])),
    html.Div(id='output1'),
    html.Div(id='output2'),
])

@app.callback(
    Output('output1', 'children'),
    [Input('select_input', 'value')],
)
def gen_text_input(select_input):
    if select_input == '2':
        temp = [dcc.Input(id='input' + str(i), type='text') for i in range(2)]
        temp.append(html.Button('submit 2', id='text_submit', n_clicks=0))
        return temp
    if select_input == '3':
        temp = [dcc.Input(id='input' + str(i), type='text') for i in range(3)]
        temp.append(html.Button('submit 3', id='text_submit', n_clicks=0))
        return temp

@app.callback(
    Output('output2', 'children'),
    [Input('text_submit', 'n_clicks')],
    [State('input0','value'),  # how to code this STATE part
     State('input1','value'),  # so it can take 2 or 3 inputs
     State('input2','value')]  # depending on the previous drop down selection
)
def result(n_clicks, *args):
    return html.P(f'You have submetted {args} {n_clicks} times.')

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

Same as before, output, inputs, and state take a fixed number of arguments and can not be modified once the server has started. This approach of trying to modify the number of arguments just won’t work.

But this approach is surely a solution to some problem you are having? If you describe the problem we may be able to help find a different solution.

Hi Damian. Thanks for clarifying this. My actual application is to build a portfolio visualiser. The first step is to ask user to input some stocks from a Dropdown tag with multiple selection allowed (similar to select_input). Then some text inputs will appear and the number of the text input is the same as the number of stocks that you select (like input0, input1, …). User will need to enter stock allocation for each of the stocks in these text input boxes. The problem is I cant reach the value that user enters coz the number of input is changing, just like the STATE part of my code. If there is no other solutions, I will probably ask user to input the stock allocation in one text input box and separate them based on, say a space, at the back end.

Aha! I think I get what you mean, the user has a variable number of inputs (based on a drop down) and you need to know how many inputs are being passed.

I handle this in my code often, what I do in my code to handle this create the maximum number of inputs but hide the ones that are not being used, e.g:

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

app = dash.Dash()

app.layout = html.Div(children=[
    html.Div(
        dcc.Dropdown(id='select_input',
                     value=2,
                     options=[{'label': '2 inputs', 'value': 2}, {'label': '3 inputs', 'value': 3}])
    ),
    dcc.Input(id='input0', type='text'),
    dcc.Input(id='input1', type='text'),
    dcc.Input(id='input2', type='text', style={'display': None}),
    html.Button('Submit', id='text_submit', n_clicks=0),
    html.Div(id='output2'),
])


@app.callback(
    Output('input2', 'style'),
    [Input('select_input', 'value')],
    [State('input2', 'style')]
)
def gen_text_input(select_input, current_style):
    if select_input == 3:
        current_style['display'] = ''
    else:
        current_style['display'] = 'none'

    return current_style


@app.callback(
    Output('output2', 'children'),
    [Input('text_submit', 'n_clicks')],
    [State('select_input', 'value'),
     State('input0', 'value'),
     State('input1', 'value'),
     State('input2', 'value')]
)
def result(n_clicks, arg_number, *input_values):
    inputs = input_values[:arg_number]
    return html.P(f'You have {arg_number} args submitted {inputs} {n_clicks} times.')


if __name__ == '__main__':
    app.run_server()
1 Like

OMG!!! This is exactly what I want. Could never be able to come up with this solution myself. Thank you so much!!!

1 Like