Ordering children with drag & drop, how to trigger "dash component has changed" from JS

Syncing UI changes via drag&drop with the server is a challenge. Mostly because:

  • Dash lacks access to the DOM
  • If a component does not react to html/DOM events than there is no way to talk back to the Dash server (e.g., double clicks)

I found a solution using EventListener, which allows to sends custom events in javascript and listen to them in Dash. Essentially, one needs to wrap the drag_container inside the EventListener and trigger a custom “dropcomplete” event when the drop has been completed.

@sdementen : I adapted your original posting to make the changes clearer.

App.py

Key line here is - EventListener(html.div(id=“drag_container”, …)) and in the callback Input(“el”, “n_events”), State(“el”, “event”)

from dash_extensions.enrich import DashProxy, html, Input, Output, ClientsideFunction, State
from dash_extensions import EventListener


app = DashProxy(
    __name__,
    external_scripts=["https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.2/dragula.min.js"],
)

event = {"event": "dropcomplete", "props": ["detail.name"]}

app.layout = html.Div(
    id="main",
    children=[
        html.Button(id="btn", children="Refresh display for order of children"),
        html.Label(id="order"),
        EventListener(
            html.Div(
            id="drag_container",
            className="container",
            children=[html.Button(id=f"child-{i}", children=f"child-{i}") for i in range(5)],
            ),
            events=[event], logging=True, id="el")
    ],
)

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("order", "children"),
    [
        Input("el", "n_events"), State("el", "event"),
        State(component_id="drag_container", component_property="children"),
    ],
)
def watch_children(nevents, event_data, children):
    """Display on screen the order of children"""
    return ", ".join([comp["props"]["id"] for comp in children])


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

assets/script.js

Key line here is - new customEvent(‘dropcomplete’, …), target.dispatchEvent(drop_complete)

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)
                });

                const drop_complete = new CustomEvent('dropcomplete', {
                    bubbles: true,
                    detail: {
                      name: "Additional event infos"
                    }
                  });
                target.dispatchEvent(drop_complete)
                // How can I trigger an update on the children property
                // ???
            })
        }, 1)
        return window.dash_clientside.no_update
    }
}
2 Likes