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