How To? Return JavaScript variable from clientside_callback in dcc.Store element

my goal:
I want to arrange the buttons any way I want (drag and drop via JavaScript) and when I press the ‘Show new order’ button I want to pick up the displayed order (show in ‘label2’ in this example).

Current situation:
I have three buttons in a html.div. Via a clientside_callback I can rearrange the buttons. I want to pick up this new arrangement in the next step. At this stage I can only change the html.label with id=‘label1’ in the browser, not the ‘children’ itself.

Approach:
Return the order from the variable ‘order_ids’ to the dcc.Store element with the id=‘store_order’. Unfortunately, all my attempts so far have failed.

Bild1

index.py

import dash
import dash_bootstrap_components as dbc
from dash import dcc, html, Input, Output, State, ClientsideFunction

app = dash.Dash(
    __name__,
    suppress_callback_exceptions=True,
    external_scripts=["https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.2/dragula.min.js"],
    external_stylesheets=[dbc.themes.BOOTSTRAP]
)

app.layout = html.Div([
    dcc.Store(id='store_order', storage_type='session'),
    html.Div(
        children=[
            # Button No. 1
            dbc.Button(
                children='No. 1',
                id='btn_1',
                style={'margin': '10px', 'background-color': '#e9c46a', 'display': 'block'}
            ),
            # Button No. 2
            dbc.Button(
                children='No. 2',
                id='btn_2',
                style={'margin': '10px', 'background-color': '#2a9d8f', 'display': 'block'}
            ),
            # Button No. 3
            dbc.Button(
                children='No. 3',
                id='btn_3',
                style={'margin': '10px', 'background-color': '#264653', 'display': 'block'}
            ),
        ],
        id='drag_container',
        style={
            'background-color': '#ffe8d6',
            'width': '250px',
            'border': '1px solid black',
            'margin': '10px',
            'padding': '10px',
        }
    ),
    html.Div(
        [
            dbc.Button('Show new order', id='btn_order'),
            html.Br(),
            html.Label('Label 1: '),
            html.Label(id='label1'),
            html.Br(),
            html.Label('Label 2: '),
            html.Label(id='label2'),
        ],
        style={'margin': '10px'}
    ),
])


app.clientside_callback(
    ClientsideFunction(namespace='clientside', function_name='make_draggable'),
    Output('drag_container', 'data-drag'),
    [Input('drag_container', 'id')],
    [State('drag_container', 'children')],
)


@app.callback(
    Output('label2', 'children'),
    Input('btn_order', 'n_clicks'),
    State('store_order', 'data')
)
def show_new_order(n_clicks, data):
    # more code ...
    return str(data)


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

assets/scripts.js

if (!window.dash_clientside) {
    window.dash_clientside = {};
}
window.dash_clientside.clientside = {
    make_draggable: function (id, children) {
        setTimeout(function () {
            var drake = dragula({});
            var el = document.getElementById(id)
            drake.containers.push(el);
            drake.on("drop", function (_el, target, source, sibling) {
                // a component has been dragged & dropped
                // get the order of the ids from the DOM
                var order_ids = Array.from(target.children).map(function (child) {
                    return child.id;
                });
                // in place sorting of the children to match the new order
                children.sort(function (child1, child2) {
                    return order_ids.indexOf(child1.props.id) - order_ids.indexOf(child2.props.id)
                });
                // Trigger an update on the children property
                label1.innerHTML = order_ids;
            })
        }, 1)
        return window.dash_clientside.no_update
    }
}

assets/dragula.css

.gu-mirror {
    position: fixed !important;
    margin: 0 !important;
    z-index: 9999 !important;
    opacity: 0.8;
    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
    filter: alpha(opacity=80);
  }
  .gu-hide {
    display: none !important;
  }
  .gu-unselectable {
    -webkit-user-select: none !important;
    -moz-user-select: none !important;
    -ms-user-select: none !important;
    user-select: none !important;
  }
  .gu-transit {
    opacity: 0.2;
    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
    filter: alpha(opacity=20);
  }

Hi @BaCode,

This is a very interesting question, and I am commenting with hopes that somebody can give you a nice answer…

The biggest challenge you’ll face with this approach is that the clientside callback is used just for side-effects, and it won’t trigger any other updates in other callbacks. You can’t update dcc.Store from the clientside either, because your component state will be out of sync with the session storage that holds the data on the window object clientside…

Unfortunately I don’t know enough about the Dash internals to say if there is a way to trigger a component update other than in a clientside callback (by returning something in one). If there is a way, then you could use it in the end of your event handler just as you did to update the innerHTML of the label.

Another alternative, perhaps more elegant, would be to use the React version of dragula in a custom component. There you’ll have a better control of the component state and will be easier to couple events to props in the Dash components.

Hope that this makes some sense and is somehow helpful… :smiley:

@BaCode I believe it should be possible to collect a client side variable into a Store element by targeting the store as an output in a client side callback, i.e. something along the lines of

app.clientside_callback(
    ""  // Put JavaScript code that collects the desired state here
    Output('some_store', 'data'),
    [Input('drag_container', 'data-drag')],
)

That being said, I agree with @jlfsjunior that this approach is not recommended. Bypassing Dash (and React) might lead to all kinds of issues down the road, so I would say that the better solution would be to create a Dash wrapper for the desired library (dragula) and use that instead.

This can be achieved with using EventListener from dash_extensions in combination with CustomEvent in your make_draggable function (see Ordering children with drag & drop, how to trigger “dash component has changed” from JS - Dash Python - Plotly Community Forum).

This way you can pass the order_ids in the custom event and work with it further in a normal callback.

1 Like