Download getting triggered automatically after removing element

dash_issue

As you can see from this if I press the download button the download pop-up comes. After downloading, if I try to delete other cards, the download popup comes again. Why is it happening?

Here is a minimal reproducible example

STEPS to reproduce

  1. Press the download button and download or cancel whatever you like
  2. Now try to delete some other card by pressing the delete button

Observation:
The download pop up should come again

import json

import dash
import dash_bootstrap_components as dbc
from dash import dcc
from dash.dependencies import Input, Output, ALL, State

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

app.layout = dbc.Container([
    dbc.Row([
        dcc.Download(id="download-1"),
        'card 1',
        dbc.Col(dbc.Button('download', id='download-btn-1')),
        dbc.Col(dbc.Button('delete', id={'type': 'delete', 'id': 'card-1'}))
    ], className='bg-warning m-2 p-2', id='card-1'),
    dbc.Row([
        dcc.Download(id="download-2"),
        'card 2',
        dbc.Col(dbc.Button('download', id='download-btn-2')),
        dbc.Col(dbc.Button('delete', id={'type': 'delete', 'id': 'card-2'}))
    ], className='bg-warning m-2 p-2', id='card-2'),
    dbc.Row([
        dcc.Download(id="download-3"),
        'card 3',
        dbc.Col(dbc.Button('download', id='download-btn-3')),
        dbc.Col(dbc.Button('delete', id={'type': 'delete', 'id': 'card-3'}))
    ], className='bg-warning m-2 p-2', id='card-3'),
], id='container-body')


@app.callback(
    Output('download-1', 'data'),
    Input('download-btn-1', 'n_clicks'),
    prevent_initial_call=True
)
def download_1(n_clicks):
    return dict(content="data 1", filename="hello.txt")


@app.callback(
    Output('download-2', 'data'),
    Input('download-btn-2', 'n_clicks'),
    prevent_initial_call=True
)
def download_1(n_clicks):
    return dict(content="data 2", filename="hello.txt")


@app.callback(
    Output('download-3', 'data'),
    Input('download-btn-3', 'n_clicks'),
    prevent_initial_call=True
)
def download_1(n_clicks):
    return dict(content="data 3", filename="hello.txt")


@app.callback(
    Output('container-body', 'children'),
    Input({'type': 'delete', 'id': ALL}, 'n_clicks'),
    State('container-body', 'children'),
    prevent_initial_call=True
)
def delete_children(n_clicks, children):
    card_id_to_be_deleted = json.loads(dash.callback_context.triggered[0]['prop_id'].split('.')[0])['id']
    index_to_be_deleted = None
    for index, c in enumerate(children):
        if c['props']['id'] == card_id_to_be_deleted:
            index_to_be_deleted = index
            break
    children.pop(index_to_be_deleted)
    return children


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

This is happening because you are rewriting the buttons in the delete_children callback, and the download_* callbacks are getting evaluated again (with n_clicks not None for the clicked buttons).

One solution could be:

  1. To add a conditional preventing download_ to update if n_clicks is None:
@app.callback(
    Output('download-3', 'data'),
    Input('download-btn-3', 'n_clicks'),
)
def download_3(n_clicks):
    if n_clicks is not None:
        return dict(content="data 3", filename="hello.txt")
  1. Reset n_clicks to None inside delete_children. So, just add something like this before the return:
for child in children:
    # download_btn = child["props"]["children"][1]
    # download_btn["props"]["n_click"] should reset
    child["props"]["children"][1]["props"]["n_click"] = None

There might be some better ways to do it

Hi @ jlfsjunior Thanks for the reply, but I don’t understand how am I rewriting the buttons? I am just removing one child and giving the remaining chidren. Does this rewrite the button? What is the better way to remove chindren in dash then?

I don’t understand how am I rewriting the buttons? I am just removing one child and giving the remaining chidren. Does this rewrite the button?

Yes, it does. It may not look like it, as if you inspect children, you will see a nested dictionary with the JSON representation of the components defined in the dbc.Container, but effectively you are just rewriting them.

Dash does not support partial updates of props (props being the “parameters” you can pass to a component when initialising it or in a callback), so I don’t think there is a better way to do what you are doing. You just have to work with it and be aware that the callbacks depending on components being rewritten will be triggered again.

1 Like

Hi @ jlfsjunior I have implemented your suggestion but not getting a positive outcome. Can you see if this the one you suggested?

import json

import dash
import dash_bootstrap_components as dbc
from dash import dcc
from dash.dependencies import Input, Output, ALL, State

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

app.layout = dbc.Container([
    dbc.Row([
        dcc.Download(id="download-1"),
        'card 1',
        dbc.Col(dbc.Button('download', id='download-btn-1')),
        dbc.Col(dbc.Button('delete', id={'type': 'delete', 'id': 'card-1'}))
    ], className='bg-warning m-2 p-2', id='card-1'),
    dbc.Row([
        dcc.Download(id="download-2"),
        'card 2',
        dbc.Col(dbc.Button('download', id='download-btn-2')),
        dbc.Col(dbc.Button('delete', id={'type': 'delete', 'id': 'card-2'}))
    ], className='bg-warning m-2 p-2', id='card-2'),
    dbc.Row([
        dcc.Download(id="download-3"),
        'card 3',
        dbc.Col(dbc.Button('download', id='download-btn-3')),
        dbc.Col(dbc.Button('delete', id={'type': 'delete', 'id': 'card-3'}))
    ], className='bg-warning m-2 p-2', id='card-3'),
], id='container-body')


@app.callback(
    Output('download-1', 'data'),
    Input('download-btn-1', 'n_clicks'),
    prevent_initial_call=True
)
def download_1(n_clicks):
    if n_clicks is not None:
        return dict(content="data 1", filename="hello1.txt")


@app.callback(
    Output('download-2', 'data'),
    Input('download-btn-2', 'n_clicks'),
    prevent_initial_call=True
)
def download_1(n_clicks):
    if n_clicks is not None:
        return dict(content="data 2", filename="hello2.txt")


@app.callback(
    Output('download-3', 'data'),
    Input('download-btn-3', 'n_clicks'),
    prevent_initial_call=True
)
def download_1(n_clicks):
    print(n_clicks)
    if n_clicks is not None:
        return dict(content="data 3", filename="hello3.txt")


@app.callback(
    Output('container-body', 'children'),
    Input({'type': 'delete', 'id': ALL}, 'n_clicks'),
    State('container-body', 'children'),
    prevent_initial_call=True
)
def delete_children(n_clicks, children):
    card_id_to_be_deleted = json.loads(dash.callback_context.triggered[0]['prop_id'].split('.')[0])['id']
    index_to_be_deleted = None
    for index, c in enumerate(children):
        c["props"]["children"][2]["props"]["n_click"] = None
        c["props"]["children"][3]["props"]["n_click"] = None
        if c['props']['id'] == card_id_to_be_deleted:
            index_to_be_deleted = index
            break
    children.pop(index_to_be_deleted)
    return children


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

Using clientside callback, this issue has been resolved though

import json

import dash
import dash_bootstrap_components as dbc
from dash import dcc
from dash.dependencies import Input, Output, ALL, State

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

app.layout = dbc.Container([
    dcc.Store(id='child-index-to-remove'),
    dbc.Row([
        dcc.Download(id="download-1"),
        'card 1',
        dbc.Col(dbc.Button('download', id='download-btn-1')),
        dbc.Col(dbc.Button('delete', id={'type': 'delete', 'id': 'card-1'}))
    ], className='bg-warning m-2 p-2', id='card-1'),
    dbc.Row([
        dcc.Download(id="download-2"),
        'card 2',
        dbc.Col(dbc.Button('download', id='download-btn-2')),
        dbc.Col(dbc.Button('delete', id={'type': 'delete', 'id': 'card-2'}))
    ], className='bg-warning m-2 p-2', id='card-2'),
    dbc.Row([
        dcc.Download(id="download-3"),
        'card 3',
        dbc.Col(dbc.Button('download', id='download-btn-3')),
        dbc.Col(dbc.Button('delete', id={'type': 'delete', 'id': 'card-3'}))
    ], className='bg-warning m-2 p-2', id='card-3'),
], id='container-body')


@app.callback(
    Output('download-1', 'data'),
    Input('download-btn-1', 'n_clicks'),
    prevent_initial_call=True
)
def download_1(n_clicks):
    if n_clicks is not None:
        return dict(content="data 1", filename="hello1.txt")


@app.callback(
    Output('download-2', 'data'),
    Input('download-btn-2', 'n_clicks'),
    prevent_initial_call=True
)
def download_1(n_clicks):
    if n_clicks is not None:
        return dict(content="data 2", filename="hello2.txt")


@app.callback(
    Output('download-3', 'data'),
    Input('download-btn-3', 'n_clicks'),
    prevent_initial_call=True
)
def download_1(n_clicks):
    print(n_clicks)
    if n_clicks is not None:
        return dict(content="data 3", filename="hello3.txt")


@app.callback(
    Output('child-index-to-remove', 'data'),
    Input({'type': 'delete', 'id': ALL}, 'n_clicks'),
    State('container-body', 'children'),
    prevent_initial_call=True
)
def delete_children(n_clicks, children):
    card_id_to_be_deleted = json.loads(dash.callback_context.triggered[0]['prop_id'].split('.')[0])['id']
    index_to_be_deleted = None
    for index, c in enumerate(children):
        if c['props']['id'] == card_id_to_be_deleted:
            index_to_be_deleted = index
            break
    return index_to_be_deleted


app.clientside_callback(
    """
    function(index, children) {
    if(index){
        children.splice(index, 1);
        return children;
        }
    else{
        return children;
        }
    }
    """,
    Output('container-body', 'children'),
    Input('child-index-to-remove', 'data'),
    State('container-body', 'children')
)

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

This is actually a much better solution than mine!

You can also combine the two last callbacks into one, as callback_context is available on clientside too. Using clientside to prevent triggering those callbacks is actually a pretty neat trick, so thanks for following up on that and I certainly learned something today! :smiley:

1 Like

As suggested by @ jlfsjunior we can use clientside callback context to simplify the solution.
Here is the solution

import json

import dash
import dash_bootstrap_components as dbc
from dash import dcc
from dash.dependencies import Input, Output, ALL, State

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

app.layout = dbc.Container([
    dbc.Row([
        dcc.Download(id="download-1"),
        'card 1',
        dbc.Col(dbc.Button('download', id='download-btn-1')),
        dbc.Col(dbc.Button('delete', id={'type': 'delete', 'id': 'card-1'}))
    ], className='bg-warning m-2 p-2', id='card-1'),
    dbc.Row([
        dcc.Download(id="download-2"),
        'card 2',
        dbc.Col(dbc.Button('download', id='download-btn-2')),
        dbc.Col(dbc.Button('delete', id={'type': 'delete', 'id': 'card-2'}))
    ], className='bg-warning m-2 p-2', id='card-2'),
    dbc.Row([
        dcc.Download(id="download-3"),
        'card 3',
        dbc.Col(dbc.Button('download', id='download-btn-3')),
        dbc.Col(dbc.Button('delete', id={'type': 'delete', 'id': 'card-3'}))
    ], className='bg-warning m-2 p-2', id='card-3'),
], id='container-body')


@app.callback(
    Output('download-1', 'data'),
    Input('download-btn-1', 'n_clicks'),
    prevent_initial_call=True
)
def download_1(n_clicks):
    if n_clicks is not None:
        return dict(content="data 1", filename="hello1.txt")


@app.callback(
    Output('download-2', 'data'),
    Input('download-btn-2', 'n_clicks'),
    prevent_initial_call=True
)
def download_1(n_clicks):
    if n_clicks is not None:
        return dict(content="data 2", filename="hello2.txt")


@app.callback(
    Output('download-3', 'data'),
    Input('download-btn-3', 'n_clicks'),
    prevent_initial_call=True
)
def download_1(n_clicks):
    print(n_clicks)
    if n_clicks is not None:
        return dict(content="data 3", filename="hello3.txt")


app.clientside_callback(
    """
    function(n_clicks, children) {
        const triggered = dash_clientside.callback_context.triggered.map(t => t.prop_id);
        const card_id_to_remove = JSON.parse(triggered[0].split('.')[0])['type'];
        let child_index_to_remove = null;
        for(let i=0; i<children.length; i++){
            if (children[i]['props']['id'] === card_id_to_remove){
                child_index_to_remove = i;
                break;
            }
        }
        children.splice(child_index_to_remove, 1);
        return children;
    }
    """,
    Output('container-body', 'children'),
    Input({'type': 'delete', 'id': ALL}, 'n_clicks'),
    State('container-body', 'children'),
    prevent_initial_call=True
)

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

Related stackoverflow discussion here