Good day, All,
I recently encountered an issue where I was wanting to give feedback to my clients after immediately interacting with the application, starting a process that could take a bit. One way that I found that is really nice is the Notification by DMC.
The problem with using a notification, when passing back a component it needed to have a response from the server, meaning a roundtrip in order to complete the task. This isnt a big issue if you are really close to the server, but create a gap and you will notice a huge lag in the button push and the desired feedback that they triggered an event and process.
It is also an issue if you have a keydown listener and the user doesnt know that they actually triggered the event, thus potentially stacking the events.
Anyways, I decided to tackle a question that has been bugging me, is it possible to provide a component in a clientside callback.
Short answer, yes. Longer answer, there are a few ways that we can achieve this.
First thing, we need to know that the component language changes a bit:
html.Div("test")
→ {'props': {'children': 'test'}, 'type': 'Div', 'namespace': 'dash_html_components'}
A cool thing to see this is using to_plotly_json
, eg:
html.Div("test").to_plotly_json()
Using this cool feature, we can actually just insert this into a clientside callback like this:
app.clientside_callback(
"""function (n) {
if (n) {
return """ + json.dumps(html.Div("test").to_plotly_json()) + """
}
return window.dash_clientside.no_update
}""",
Output("test", "children"), Input("testing", "n_clicks")
)
^ this works, but isnt really dynamic, taking this same example and the above string from we can do this:
app.clientside_callback(
"""function (n) {
if (n) {
return {'type': 'Div', 'namespace': 'dash_html_components', 'props': {'children': n}}
}
return window.dash_clientside.no_update
}""",
Output("test2", "children"), Input("testing2", "n_clicks")
)
Now, as we click, the number inside the other component will increase.
if we compare this to regular callback on the testing2, we have a .5 second fuse:
@app.callback(
Output("test3", "children"), Input("testing2", 'n_clicks')
)
def serverCall(n):
if n:
time.sleep(.5)
return [html.Div(n)]
return no_update
Obviously, this fails in giving immediate feedback, in the meantime users could have issues or try again.
Here is this example:
from dash import html, Output, Input, Dash, no_update
import json
import time
app = Dash(__name__)
app.layout = html.Div(
[
html.Button('testing', 'testing'), html.Button('testing2', 'testing2'),
html.Div(id='test'), html.Div(id='test2'), html.Div(id='test3')
]
)
app.clientside_callback(
"""function (n) {
if (n) {
return """ + json.dumps(html.Div("test").to_plotly_json()) + """
}
return window.dash_clientside.no_update
}""",
Output("test", "children"), Input("testing", "n_clicks")
)
app.clientside_callback(
"""function (n) {
if (n) {
return {'type': 'Div', 'namespace': 'dash_html_components', 'props': {'children': n}}
}
return window.dash_clientside.no_update
}""",
Output("test2", "children"), Input("testing2", "n_clicks")
)
@app.callback(
Output("test3", "children"), Input("testing2", 'n_clicks')
)
def serverCall(n):
if n:
time.sleep(.5)
return [html.Div(n)]
return no_update
if __name__ == "__main__":
app.run_server(debug=True)
This is cool, but lets take a more real life example, have fun with notifications:
from dash import html, Output, Input, Dash, no_update
import json
import dash_mantine_components as dmc
from dash_iconify import DashIconify
import time
app = Dash(__name__)
app.layout = html.Div(
[
dmc.NotificationsProvider([
html.Button('testing', 'testing'), html.Button('testing2', 'testing2'),
html.Div(id='test'), html.Div(id='test2')
])
]
)
app.clientside_callback(
"""function (n) {
if (n) {
return [""" + json.dumps(
dmc.Notification(id="test3", action='show', message='this is a test',
autoClose=False, loading=True, color='orange').to_plotly_json()) + """, true]
}
return [window.dash_clientside.no_update, window.dash_clientside.no_update]
}""",
Output("test", "children"), Output("testing", "disabled"),
Input("testing", "n_clicks"), prevent_initial_call=True
)
@app.callback(
Output("test", "children", allow_duplicate=True),
Output("testing", "disabled", allow_duplicate=True),
Input("testing", "n_clicks"), prevent_initial_call=True
)
def serverCall(n):
if n:
time.sleep(2)
return dmc.Notification(id='test3', action='update', message='test complete',
icon=DashIconify(icon='akar-icons:circle-check'), color='green'), False
return no_update, no_update
app.clientside_callback(
"""function (n) {
if (n) {
return [{'type': 'Notification', 'namespace': 'dash_mantine_components',
'props': {'id': `${n}`, 'action': 'show', message: `this is a test ${n}`, color: 'orange', loading: true,
autoClose: false}}, true]
}
return [window.dash_clientside.no_update, window.dash_clientside.no_update]
}""",
Output("test2", "children"), Output("testing2", "disabled"), Input("testing2", "n_clicks"), prevent_initial_call=True
)
@app.callback(
Output("test2", "children", allow_duplicate=True),
Output("testing2", "disabled", allow_duplicate=True),
Input("testing2", "n_clicks"), prevent_initial_call=True
)
def serverCall2(n):
if n:
time.sleep(2)
return dmc.Notification(id=f'{n}', action='update', message=f'test {n} complete',
icon=DashIconify(icon='akar-icons:circle-check'), color='green'), False
return no_update, no_update
if __name__ == "__main__":
app.run_server(debug=True)
edit: Yes, these clientside returned components can receive updates from dash:
from dash import html, Output, Input, Dash, no_update
import json
import time
app = Dash(__name__, suppress_callback_exceptions=True)
app.layout = html.Div(
[
html.Button('testing', 'testing'), html.Button('testing2', 'testing2'), html.Button('testing4', 'testing4'),
html.Div(id='test'), html.Div(id='test2'), html.Div(id='test3')
]
)
app.clientside_callback(
"""function (n) {
if (n) {
return """ + json.dumps(html.Div(id="test4", children="test").to_plotly_json()) + """
}
return window.dash_clientside.no_update
}""",
Output("test", "children"), Input("testing", "n_clicks")
)
app.clientside_callback(
"""function (n) {
if (n) {
return {'type': 'Div', 'namespace': 'dash_html_components', 'props': {'children': n}}
}
return window.dash_clientside.no_update
}""",
Output("test2", "children"), Input("testing2", "n_clicks")
)
app.clientside_callback(
"""function (n) {
if (n) {
return {'type': 'Div', 'namespace': 'dash_html_components', 'props': {'children': n}}
}
return window.dash_clientside.no_update
}""",
Output("test4", "children"), Input("testing4", "n_clicks")
)
@app.callback(
Output("test3", "children"), Input("testing2", 'n_clicks')
)
def serverCall(n):
if n:
time.sleep(.5)
return [html.Div(n)]
return no_update
if __name__ == "__main__":
app.run_server(debug=True)