Clientside Callbacks equivalent for dash_callback context

Hello

Is there any way to know what input triggered in a clientside callback?

If I have

 app.clientside_callback(
            ClientsideFunction(
                namespace='clientside',
                function_name=my_fun'
            ),
            Output('bla', "className"),
            [
                Input("Inp-1", "n_clicks"),
                Input("Inp-2", "n_clicks"),

            ]
        )

and

if(!window.dash_clientside) {window.dash_clientside = {};}

window.dash_clientside.clientside = {

    my_fun:(...args) => {
         ...

    }
}

I will not be able (?) to know which input triggered. Is there a way to know?

Love Clientside BTW!

Edit 1: Typo
Edit 2: Typo

2 Likes

Any solution to this?

This has not been implemented, but it’s definitely a good idea and there isn’t anything technical blocking it from happening (just development time!). GitHub issue here: https://github.com/plotly/dash/issues/1213

1 Like

Thank you very much for opening the issue @chriddyp!

I was wondering if there was an easy workaround in the meantime. I have a clientside callback that receives different button n_clicks events as inputs, and should behave differently depending on which button was responsible for the call. Is there an obvious way to implement that? My javascript skills are limited, and I have trouble following what is happening in the guts of react/dash/…

@nicoco - You can listen to n_clicks_timestamp (instead of n_clicks). By comparing the timestamps, you can get the trigger, i.e. the one with the latest timestamps.

3 Likes

Thank you for your answer! However, my problem is that not all inputs for this client side callback are buttons, one is storage data modifications, another one is graph clickData and in fact only one is an actual button. This is probably poor design on my side…

It gets even uglier, as a workaround, I made this callback output to n_clicks_timestamp of the button that triggers it and also receive n_click_timestamp as a State so that I can determine what actually called the callback. Yeah it is messy. It works ATM but looks unmaitainable, have to redesign this.

Is this feature added in dash==1.12.0 ?
I cant see any field “triggered” or even “callback_context” inside “dash_clientside”

Have a look here - it was just released! :smile:

1 Like

Great! So glad I found this. It works perfectly. Why isn’t this feature in the documentation? It’s been so many months.

2 Likes

Just to be clear (since the answer marked “Solution” points to the Github releases page), this feature was released in 1.13.

2 Likes

Adding the associated forum release post for v1.13 since it’s one of the top results.

From the release post:

window.dash_clientside.callback_context
window.dash_clientside.callback_context.triggered
window.dash_clientside.callback_context.inputs
window.dash_clientside.callback_context.inputs_list
window.dash_clientside.callback_context.states
window.dash_clientside.callback_context.states_list
3 Likes

is there an equivalent to ctx.outputs_list

The callback context applies just to the Inputs. What are you trying to do with the callback outputs?

Thank you @AnnMarieW for the quick reply :slightly_smiling_face:

I was trying to use the same logic below in the client-side callback but could not find the equivalent of ctx.outputs_list. buy the way there isn’t the equivalent of ctx.triggered_id witch would make the code a bit cleaner.

@app.callback(
    Output({"type": "page_layout_id", "index": ALL}, "style"), 
    Output({"type": "page_switcher_action", "index": ALL}, "style"),
    Output({"type": "page_switcher_action_text", "index": ALL}, "style"),
    Input({"type": "page_switcher_action", "index": ALL}, "n_clicks"),
    prevent_initial_call=True
)

def page_switcher(action_click):
    print(ctx.outputs_list[0])
    triggered_input = ctx.triggered_id['index']
    output_list = [i['id']['index'] for i in ctx.outputs_list[0]]
    print(output_list)
    number_of_outputs, triggered_input_index = len(output_list), output_list.index(triggered_input)
    page_output_list = [{'display':'none'}] * number_of_outputs
    action_item_output_list = [{}] * number_of_outputs
    page_output_list[triggered_input_index] = {'display':'block'}
    action_item_output_list[triggered_input_index] = {'color':'rgb(34, 139, 230)'}

    return page_output_list, action_item_output_list, action_item_output_list

here is how i ended up converting the above serverside call back into clientside callback.

page_switcher: function(page_switcher_input) {
    ctx = window.dash_clientside.callback_context;
 
    pages_list = ctx.inputs_list[0];
    triggered_input = JSON.parse(ctx.triggered[0].prop_id.replace(".n_clicks", "")).index
    let output_list = []
    pages_list.forEach((page) => output_list.push(page.id.index));
    let number_of_pages = output_list.length
    let triggered_input_index = output_list.indexOf(triggered_input)

    let page_output_list = Array(number_of_pages).fill({'display':'none'})
    let action_item_output_list = Array(number_of_pages).fill({})
    page_output_list[triggered_input_index] = {'display':'block'}
    action_item_output_list[triggered_input_index] = {'color':'rgb(34, 139, 230)'}
    console.log(page_output_list, action_item_output_list);
  
    return [page_output_list, action_item_output_list, action_item_output_list];
}

Hi @Amazigh

It appears that you are trying to switch pages of a multi page app, Have you tried Dash Pages? Another option is to use the Tabs component with the content as tab children. Then the component itself will handle displaying the pages, no callback required. A third option is to use MATCH instead of ALL

I initially started with MATCH. Here’s what I struggled with: in the provided code, there are two callbacks. The commented one uses ALL and it functions correctly. The other one is where I attempted to use MATCH, but what I couldn’t achieve was hiding the other pages upon clicking. Below is the complete code "



from dash import Dash, dcc, html, Input, Output,  ALL,  MATCH, callback, ctx, no_update
from dash_iconify import DashIconify as icon
import dash_mantine_components as dmc

app = Dash(
    __name__,
    external_stylesheets=[
        "https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;900&display=swap"
    ],
)

footer_icon_with = 30
footer_icon_color = '#A519C7'

def id_dict (_id, index):
    return {"type": f"{_id}", "index":f"{index}"}

def footer_action_link (_id, _index, _icon, is_default=False):
    icon_text_size = 'xs'
    icon_and_text_color = {}
    if is_default:
        icon_and_text_color = {'color':'rgb(34, 139, 230)'}

    return  dmc.Center(
                children = [
                    dmc.Stack(
                        align="center",
                        children = [
                            dmc.ActionIcon(
                                id = id_dict (f'{_id}', _index),
                                variant='transparent',  
                                style = icon_and_text_color,
                                children = [
                                    icon(icon=_icon, width = footer_icon_with)
                                ]
                            ),
                            dmc.Text("Shop", size=icon_text_size, style = icon_and_text_color, id = id_dict (f'{_id}_text', _index)),
                        ]
                    )
                ]
            ) 
    
footer  = dmc.Paper(
    style= {'position': 'fixed', 'bottom':'0', 'width': '450px'},
    children =[
        dmc.Group(
            position = 'apart',
            children = [
                footer_action_link ("page_switcher_action", "shop", 'tabler:shopping-bag', is_default=True),
                footer_action_link ("page_switcher_action", "account", 'ic:sharp-person-outline'),
                footer_action_link ("page_switcher_action", "cart", 'ic:outline-shopping-cart')
            ]
        ),
        
    ]
)



shop = dmc.Paper(
    id = id_dict('page_layout_id', 'shop'),
    style = {'display':'block'},
    className = 'page',
    children = [
        dmc.Text("cart_page", size="md", color ='blue'),

    ]
)

account = dmc.Paper(
    id = id_dict('page_layout_id', 'account'),
    className = 'page',
    style = {'display':'none'},
    children = [
        dmc.Text("account_page", size="md", color ='yellow'),
    ]
)

cart = dmc.Paper(
    id = id_dict('page_layout_id', 'cart'),
    style = {'display':'none'},
    className = 'page',
    children = [
        dmc.Text("cart_page", size="md", color ='blue'),

    ]
)
content = dmc.Container(
    className = 'content',
    children = [
        shop,
        account,
        cart,
    ]
)
   
app.layout = html.Div(
    style={"height": 930, "width": 450, 'border': '2px solid red', 'overflow':'scroll'},
    children=[
        html.Div(
            children = [
                content,
                footer
            ]
        )
    ]

)

# @app.callback(
#     Output({"type": "page_layout_id", "index": ALL}, "style"), 
#     Output({"type": "page_switcher_action", "index": ALL}, "style"),
#     Output({"type": "page_switcher_action_text", "index": ALL}, "style"),
#     Input({"type": "page_switcher_action", "index": ALL}, "n_clicks"),
#     prevent_initial_call=True
# )

# def page_switcher(action_click):
#     print(ctx.outputs_list[0])
#     triggered_input = ctx.triggered_id['index']
#     output_list = [i['id']['index'] for i in ctx.outputs_list[0]]
#     print(output_list)
#     number_of_outputs, triggered_input_index = len(output_list), output_list.index(triggered_input)
#     page_output_list = [{'display':'none'}] * number_of_outputs
#     action_item_output_list = [{}] * number_of_outputs
#     page_output_list[triggered_input_index] = {'display':'block'}
#     action_item_output_list[triggered_input_index] = {'color':'rgb(34, 139, 230)'}

#     return page_output_list, action_item_output_list, action_item_output_list


@app.callback(
    Output({"type": "page_layout_id", "index": MATCH}, "style"), 
    Input({"type": "page_switcher_action", "index": MATCH}, "n_clicks"),
    prevent_initial_call=True
)

def page_switcher(action_click):
  
    return {'display':'block'}


if __name__ == "__main__":
    app.run_server(debug=True, host='0.0.0.0', port=8050 )

Hi @Amazigh - Before digging into this, can you say why Pages or Tabs won’t work for you? It would be a much easier solution.

I prefer not to utilize pages since I aim to avoid making server calls for page switching. Unfortunately, dmc.Tabs lacks an option to position the navigation at the bottom. The provided code offers a workaround using ALL, but I’m keen to explore your suggestion of using MATCH, which would be a cleaner approach and an opportunity to explore another practical application for MATCH.

Hello @Amazigh,

Have you looked into my page layout caching?

This loads everything to cache from the server and then caters the responses from the cache.

It looks like the latest release of Dash Mantine Components will have Tabs with the navigation on the bottom.