Plotly Dash: How to reset the "n_clicks" attribute of a dash-html.button?

Question:

I have a basic datatable in plotly/dash. My goal is to upload (or print for the sake of the example…) after I press the upload-button.

The issue is, that I can’t figure out how to get the n_clicks attribute of the button back to zero. So what happens is that after I clicked the button for the first time it prints continuously whenever something changes (row added or number changed/added), but what I want is for it to print only once whenever I click the button.

This is the code:

import dash
from dash.dependencies import Input, Output, State
import dash_table
import dash_daq as daq

import dash_core_components as dcc
import dash_html_components as html
import pandas as pd

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']


df = pd.read_csv('.../dummy_csv.csv')


app = dash.Dash(__name__)

app.layout = html.Div([
    html.Div(id='my-div'),

    dash_table.DataTable(
        id='adding-rows-table',
        style_data={
            'whiteSpace': 'normal',
            'height': 'auto'
        },
        style_table={
            'maxHeight': '800px'
            , 'overflowY': 'scroll'
        },
        columns=[
            {'name': i, 'id': i} for i in df.columns
        ],
        data=df.to_dict('records'),
        editable=True,
        row_deletable=True

    ),

    html.Button('+ Row', id='editing-rows-button', n_clicks=0),

    html.Button('Update', id='btn-nclicks-1', n_clicks=0 ),

])


@app.callback(
    Output(component_id='my-div', component_property='children'),
    [Input('btn-nclicks-1', 'n_clicks'), Input('adding-rows-table', 'data')]
)
def update_output_div(n_clicks, data):
    if n_clicks > 0:
        print(data)
        # n_clicks = 0
        # return n_clicks

    else:
        print("else loop")


@app.callback(
    Output('adding-rows-table', 'data'),
    [Input('editing-rows-button', 'n_clicks')],
    [State('adding-rows-table', 'data'),
     State('adding-rows-table', 'columns')])
def add_row(n_clicks, rows, columns):
    if n_clicks > 0:
        rows.append({c['id']: '' for c in columns})
    return rows


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

This the CSV:

a,b,c,d
1,1,5,1
2,2,5,1
2,33,6,2
3,4,6,2

And this is the "faulty" output.

Asked By: tristan

||

Answers:

You could use the dash.callback_context property to trigger the callback only when the number of clicks has changed rather than after the first click. See the section on "Determining which Button Changed with callback_context" in the Dash documentation. The following is an example of how you could update your callback.

@app.callback(Output(component_id='my-div', component_property='children'),
    [Input('btn-nclicks-1', 'n_clicks'), Input('adding-rows-table', 'data')])
def update_output_div(n_clicks, data):

    changed_id = [p['prop_id'] for p in dash.callback_context.triggered][0]

    if 'btn-nclicks-1' in changed_id:

        print(data)
        # n_clicks = 0
        # return n_clicks

    else:

        print("else loop")
Answered By: Flavia Giammarino

You can reset the n_clicks=None attribute, and it won’t trigger an infinite loop when used in combination with a PreventUpdate null condition that you are probably already using.

@callback(
    Output('some_btn', 'n_clicks'),
    Input('some_btn', 'n_clicks')
)
def upon_click(n_clicks):
    if (n_clicks is None): raise PreventUpdate
    print(n_clicks)#1
    return None

It’s a bit more explicit and faster than callback_context ‍♂️ For example, my button’s id contained a complex "pattern-matching callback" so I didn’t want to parse the callback context for that id

Answered By: Kermit

An option that worked for me is: n_clicks = 0 in the button declaration.
create list and add click data, then check if next index is greater than current index.

An approximate example with your code would be:

list_clicks = []
@app.callback(
    Output('adding-rows-table', 'data'),
    [Input('editing-rows-button', 'n_clicks')],
    [State('adding-rows-table', 'data'),
     State('adding-rows-table', 'columns')])
def add_row(n_clicks, rows, columns):

    list_clicks.append(n_clicks)

    for index in range(len(list_clicks)-1):
        if list_clicks[index + 1] > list_clicks[index]:
            list_clicks.clear()
            rows.append({c['id']: '' for c in columns})
    
    return rows

clicking on the button n_clicks is increased by 1.
What the code block does is compare if the current index is greater than the previous one.
If it is greater, it means that the button was clicked.

list_clicks.clear() it is to clean the list and not iterate over the same passed values.

maybe you could adapt it better to your code!

Answered By: NicolasDiaz
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.