Returning a completely new layout to same page in Dash

I am trying to set up a dash app which has 2 layers:

  • 1st page has a couple of input forms, and based on these inputs - app_layout
  • 2nd page - which has a different set of intputs (layout_more_inputs) which pop up depending on what 1st page input is.

I am trying to return layout_more_inputs from a callback but it doesn’t work.

app.layout = html.Div([
    html.H3('welcome to app'),html.Br(),
    dcc.Input(id='input-00-state', type='text', value='QQQ'),
    dcc.Input(id='input-01-state', type='text', value='MOVE'),
    html.Button(id='submit-button-state', n_clicks=0, children='Go!'),
    html.Div(id='output-state'),
    dcc.Graph(id='graph-with-slider'),
])

layout_more_inputs = html.Div([
    dcc.Input(id='input-10-state', type='number', value='0.11'),
    dcc.Input(id='input-11-state', type='number', value=0.12),
    html.Button(id='submit-button-state2', n_clicks=0, children='Go Go!'),
])

#front page - 0
@app.callback(
    Output('container', 'children'),
    Input('submit-button-state', 'n_clicks'),
    State('input-00-state', 'value'),
    State('input-01-state', 'value'),
)
def ask_for_more_inputs(n_clicks,asset_str,event_str):
    print("input summary:")
    print(n_clicks,asset_str,event_str)
    return layout_more_inputs #<<--DOES NOT WORK

#front page - 1
@app.callback(
    Output('graph-with-slider', 'figure'),
    Output('output-state', 'children'),
    Input('submit-button-state2', 'n_clicks'),
    State('input-10-state', 'value'),
    State('input-11-state', 'value'),
)
def more_inputs(n_clicks,input0,input1):
    d = {'x': [input0, input1], 'y': [input0, input1]}
    df = pd.DataFrame(data=d)
    filtered_df = df

    fig = px.scatter(filtered_df, x="x", y="y")
    fig.update_layout(transition_duration=500)
    return fig, u'''Button pressed {} times, 1 is "{}", and 2 is "{}"'''.format(n_clicks,state1,state2)

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

Your first callback has Output('container', 'children'), which does not exist in your layout.

Got it, thank you. How do I create something which isn’t on the layout when I first load it, but appears after a button is clicked?

You can do it by adding a Div where you want to insert layout_more_inputs with id="container" (this is missing in your layout).

If you want to rewrite the entire page, just target your outermost div block instead. This is the approach used in Multi-Page Applications, please take a look in the examples there.

Tried doing that, 2 issues appear, either ‘duplicate callback outputs’, or, when I try the below, buttons input-X-state and input-Y-state are always visible (trying to get them to appear only once ‘Go!’ is clicked).

app.layout = html.Div([
    dcc.Location(id='url', refresh=False),
    dcc.Input(id='input-00-state', type='text', value='QQQ'),
    dcc.Input(id='input-01-state', type='text', value='MOVE'),
    html.Button(id='submit-button-state', n_clicks=0, children='Go!'),
    html.Div(id='page-content1'),
])

@app.callback(
    Output('page-content1', 'children'),
    Input('submit-button-state', 'n_clicks'),
    State('input-00-state', 'value'),
    State('input-01-state', 'value'),
)
def ask_for_more_inputs(n_clicks,asset_str,event_str):
    print("n clicks: ", n_clicks)
    return html.Div([
                    dcc.Input(id='input-X-state', type='number', value=2),
                    dcc.Input(id='input-Y-state', type='number', value=3),
                    ])

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

I see… Two minor modifications to make them appear just when it is clicked:

  1. Remove n_clicks=0 from the button.
  2. Replace the callback by:
def ask_for_more_inputs(n_clicks,asset_str,event_str):
    if not n_clicks:
         raise dash.exceptions.PreventUpdate    

    return html.Div([
                    dcc.Input(id='input-X-state', type='number', value=2),
                    dcc.Input(id='input-Y-state', type='number', value=3),
                    ])

The reason why it is always appearing is because your button click count is in zero and Dash evaluates the callback when the application is initialize. Now the callback won’t trigger any updates in the output unless the button is clicked.

This worked beautifully, thank you so much!!!

Just one small issue I ran into with this down the line is the next callback - this callback gives an error because when page is first loaded the input-X-state is not yet defined before the ask_for_more_inputs button is triggered

@app.callback(
    Output('graph-with-slider', 'figure'),
    Input('submit-button-state', 'n_clicks'),
    State('move-input-0-state', 'value'),
    State('move-input-1-state', 'value'),
    State('input-X-state', 'value'),
    State('input-Y-state', 'value'),
             )
def display_value0(n_clicks,v0,v1,v2,v3):
    d = {'x': [v2, v2], 'y': [v2, v2]}
    df = pd.DataFrame(data=d)
    filtered_df = df

    fig = px.scatter(filtered_df, x="x", y="y")
    fig.update_layout(transition_duration=500)
    return fig

Yes, it happens in this case. To solve it, you can define app.validation_layout with all components you’ll use in a callback, including those that are not in the initial layout. This is explained in the last section of the Multi-Page App documentation that I linked before.

Thank you so much! I couldn’t get that to work for some reason, using this:

app.validation_layout = html.Div([
    layout_query_move,
    layout_query_parabolic,
    layout_menu,
    layout_query_menu,
    layout_optimise,
    dcc.Input(id='input-X-state', type='number', value=2),
    dcc.Input(id='input-Y-state', type='number', value=3),

    dcc.Input(id='input-XX-state', type='number', value=6),
    dcc.Input(id='input-YY-state', type='number', value=7),
    dcc.Input(id='input-Z-state',  type='number', value=8),
])

flask.has_request_context() == False

@app.callback(
    Output('graph-with-slider', 'figure'),
    Input('submit-button-state', 'n_clicks'),
    State('query-input-0-state', 'value'),
    State('query-input-1-state', 'value'),
    State('input-X-state', 'value'),
    State('input-Y-state', 'value'),
    State('input-XX-state', 'value'),
    State('input-YY-state', 'value'),
    State('input-Z-state', 'value'),
             )
def display_value0(n_clicks,v0,v1, v2,v3,v4,v5,v6):
    d = {'x': [v2, v2], 'y': [v2, v2]}
    df = pd.DataFrame(data=d)
    filtered_df = df
    fig = px.scatter(filtered_df, x="x", y="y")
    fig.update_layout(transition_duration=500)
    return fig

Also tried adding this:

flask.has_request_context() == False

The error message looks like it’s just not recognising the the validation layout and is based on the current layout that’s loaded

A nonexistent object was used in an `State` of a Dash callback. The id of this object is 
`input-X-state` and the property is `value`. 
The string ids in the current layout are: [url, page-content, query-input-0-state, query-input-1-state, query-input-2-state, full-input-boxes, submit-button-state2, submit-button-state, graph-with-slider]

Ok. How is your app.layout defined? Is it a function?

Just as a hardcoded defitinion:

app.layout = html.Div([
    dcc.Location(id='url', refresh=False),
    html.Div(id='page-content'),
])

Got it, this should not be a problem…

Maybe I am a bit lost in all the snippets, so just want to check one more thing. Are both callbacks triggered by the same input Input('submit-button-state', 'n_clicks'), the one to add extra dcc.Input to the UI and the one to update the Graph with the different inputs (from state)?

I am not entirely sure how the validation layout is used in the internals, but it could easily be a bug if both callbacks are fired by the same component and one depends on elements created by the other. Or maybe it was just a typo when you wrote the reply.

Thank you @jlfsjunior

I’m actually using a different input to each callback, and following the multipage dash template as per your advice.

The whole code is this:

layouts.py

layout_menu = html.Div([
    dcc.Link('Run Query', href='/apps/query'),html.Br(),
    dcc.Link('Optimise', href='/apps/optimise'),html.Br(),
])

#Set 0 of input boxes - PROBLEM 1
layout_query_move = html.Div([
                        dcc.Input(id='input-X-state', type='number', value=2),html.Br(),
                        dcc.Input(id='input-Y-state', type='number', value=3),html.Br(),
                        ])

#Set 1 of input boxes - PROBLEM 2
layout_query_parabolic = html.Div([
                        dcc.Input(id='input-XX-state', type='number', value=6),html.Br(),
                        dcc.Input(id='input-YY-state', type='number', value=7),html.Br(),
                        dcc.Input(id='input-Z-state', type='number', value=8),html.Br(),
                        ])

layout_query_menu = html.Div([
    dcc.Link('Go to Main', href='/apps/'),html.Br(),
    html.H3('Enter settings for Move'),
    dbc.Label("Ticker:        ", size="md"),dcc.Input(id='query-input-0-state', type='text', value='QQQ'),
    dbc.Label("Event:         ", size="md"),dcc.Input(id='query-input-1-state', type='text', value='MOVE'),
    html.Div(id='full-input-boxes'),
    html.Button(id='submit-button-state2', n_clicks=0, children='Show all inputs'),
    html.Button(id='submit-button-state', n_clicks=0, children='Go!'),
    dcc.Graph(id='graph-with-slider'),
])

The callbacks file declares 2 callbacks - one for the main page - and the other to show additional inputs which depends on the input into query-input-1-state.

callbacks.py

#Validation layout to 'declare' all the input values
app.validation_layout = html.Div([ 
    layout_query_move,
    layout_query_parabolic,
    layout_menu,
    layout_query_menu,
    layout_optimise,
    dcc.Input(id='input-X-state', type='number', value=2), #set 0 of inputs
    dcc.Input(id='input-Y-state', type='number', value=3), #set 0 of inputs

    dcc.Input(id='input-XX-state', type='number', value=6), #set 1 of inputs
    dcc.Input(id='input-YY-state', type='number', value=7), #set 1 of inputs
    dcc.Input(id='input-Z-state',  type='number', value=8), #set 1 of inputs
])

flask.has_request_context() == False

@app.callback(
    Output('graph-with-slider', 'figure'),
    Input('submit-button-state', 'n_clicks'),
    State('query-input-0-state', 'value'),
    State('query-input-1-state', 'value'),
    State('input-X-state', 'value'),
    State('input-Y-state', 'value'),
    State('input-XX-state', 'value'),
    State('input-YY-state', 'value'),
    State('input-Z-state', 'value'),
             )
def display_value0(n_clicks,v0,v1, v2,v3,v4,v5,v6):
    d = {'x': [v2, v2], 'y': [v2, v2]}
    df = pd.DataFrame(data=d)
    filtered_df = df
    fig = px.scatter(filtered_df, x="x", y="y")
    fig.update_layout(transition_duration=500)
    return fig


@app.callback(
    Output('full-input-boxes', 'children'),
    Input('submit-button-state2', 'n_clicks'),
    State('query-input-1-state', 'value'),
)
def ask_for_more_inputs(n_clicks,event_id): #,asset_str,event_str
    if not n_clicks: raise dash.exceptions.PreventUpdate
    if event_id == 'MOVE': return layout_query_move
    if event_id == 'PARABOLIC': return layout_query_parabolic

The app entry page is this (declares the app layout):

index.py #App Entry page

#MAIN LAYOUT
app.layout = html.Div([
    dcc.Location(id='url', refresh=False),
    html.Div(id='page-content'),
])

@app.callback(
        Output('page-content', 'children'),
        Input('url', 'pathname')
             )
def display_page(pathname):
    if pathname == '/apps/':
         return layout_menu
    elif pathname == '/apps/query':
         return layout_query_menu
    elif pathname == '/apps/optimise':
         return layout_optimise
    elif pathname == '/apps/move':
         return layout_query_move

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

import dash

app = dash.Dash(__name__, suppress_callback_exceptions=True)
server = app.server
1 Like

As you have suppress_callback_exceptions=True and app.validation_layout correctly defined, I would imagine that your problem is not in the validation, but when the callbacks get executed. Is the error raised when you first load the page or just when you navigate to “/apps/query”?

If it is the latter (when nav’ing to /apps/query), then the issue might be that Dash is executing (initial execution) the callbacks that were just added to the layout and, then, the states in the first callback won’t be all simultaneously in the layout. I never saw such pattern before and I don’t know if there is a proper way of fixing it.

As an alternative, I would consider changing the problematic callback by:

@app.callback(
    Output("graph-with-slider", "figure"),
    Input("submit-button-state", "n_clicks"),
    State("query-input-0-state", "value"),
    State("query-input-1-state", "value"),
    State("full-input-boxes", "children"),

)
def display_value0(n_clicks, v0, v1, children):

    if children:
        # Will be smth like [{'id': 'input-X-state', 'value': 2}, ...]
        # so just add your logic here to deal when you need X Y or XX YY Z
        inputs = [i["props"] for i in children["props"]["children"] if i["type"] == "Input"]

    # rest of the callback

The idea is to go look into the dynamic block (children) you are adding with the other callback and extract the values from there. You will have to define v2, v3 or v4, v5, v6 inside the callback, but at least all your states exist in the layout if /apps/query is open.

Please let us know if this works!

1 Like

This worked beautifully!! Thank you so much!!

This is a really elegant way of solving it - never realised you could group objects like this and perform logical operations on their existence.