I have the following Dash app (simplified). It displays text boxes and a list of words (buttons). You click the buttons that have text to add the corresponding text to the text box (this is the part I want a client-side callback for). There is another callback with the same output (now that that’s permitted with Dash 2.9) which triggers when the user clicks the “->” button which refreshes the terms.
import re
import dash
import dash_bootstrap_components as dbc
import pandas as pd
from dash import dcc, html
from dash.dependencies import ALL, Input, Output, State
from dash.exceptions import PreventUpdate
EMPTY_DIV = html.Div()
# Div for sample list of terms
WORDS = list(pd.read_csv('font_terms.csv')['adj'])
word_items = [html.Li(children=dbc.Button(word, id={'type': 'fill-word-button', 'index': i})) for i, word in enumerate(WORDS)]
terms_div = html.Div(id='terms', children=word_items)
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP], prevent_initial_callbacks="initial_duplicate")
server = app.server
app.layout = html.Div([
dcc.Store(id='terms-store', data=WORDS),
html.Div(id="adj-inputs", className="column",
children= [dcc.Input(id={"type": "adj-input", "index": i}, value='') for i in range(5)]),
terms_div,
html.Button("→", id='forward-button', n_clicks=0),])
@app.callback(
[Output({"type": "adj-input", "index": i}, 'value') for i in range(5)],
Input({"type": "fill-word-button", "index": ALL}, "n_clicks"),
State({'type': 'adj-input', 'index': ALL}, "value")
)
def fill_word_button(button_click, adj_inputs):
ctx = dash.callback_context
if not ctx.triggered:
raise PreventUpdate
button_id = ctx.triggered[0]['prop_id'].split('.')[0]
button_index = int(re.search(r"\d+", button_id).group())
# check if there is an empty text box
if '' not in adj_inputs:
raise PreventUpdate
adj_inputs = ['' if a is None else a for a in adj_inputs] + [WORDS[button_index]]
adj_inputs = list(set([a for a in adj_inputs if a!=''])) + [a for a in adj_inputs if a=='']
return adj_inputs[:5]
# The following callback, once debugged, should replace the callback above ("fill_word_button")
# @app.clientside_callback(
# """
# function(n_clicks, adj_inputs, words) {
# const ctx = dash_clientside.callbackContext;
# if (!ctx.triggered.length) {
# return adj_inputs;
# }
# const button_id = ctx.triggered[0]['prop_id'].split('.')[0];
# const button_index = parseInt(button_id.match(/\d+/)[0]);
# // check if there is an empty text box
# if (adj_inputs.every(a => a !== '')) {
# return adj_inputs;
# }
# adj_inputs = adj_inputs.concat([words[button_index]]);
# adj_inputs = [...new Set(adj_inputs.filter(a => a !== '')), ...adj_inputs.filter(a => a === '')];
# return adj_inputs.slice(0, 5);
# }
# """,
# Output({"type": "adj-input", "index": ALL}, 'value'),
# Input({"type": "fill-word-button", "index": ALL}, "n_clicks"),
# State({'type': 'adj-input', 'index': ALL}, "value"),
# State('terms-store', 'data')
# )
@app.callback(
[Output({"type": "adj-input", "index": i}, 'value', allow_duplicate=True) for i in range(5)],
[Input("forward-button", "n_clicks")],
[State({'type': 'adj-input', 'index': ALL}, "value")],
)
def refresh_terms(forward_clicks, adj_inputs):
ctx = dash.callback_context
if not ctx.triggered:
raise PreventUpdate
button_id = ctx.triggered[0]['prop_id'].split('.')[0]
if button_id == "forward-button":
adj_inputs = [adj for adj in adj_inputs if adj !=""]
# Do something with the adj_inputs e.g. upload to database and
# db.insert_response_mysql(adj_inputs)
# Reset adj_inputs
return [""]*5
if __name__ == "__main__":
app.run_server(debug=True, port=8000, host="0.0.0.0")
The error I have is connected to the refresh_terms
callback
[State({'type': 'adj-input', 'index': ALL}, "value")],
TypeError: 'NoneType' object is not callable
This only happens when I try using the client-side callback but is fine when I use a separate callback. Please help me debug this error.