Announcing Dash Bio 1.0.0 🎉 : a one-stop-shop for bioinformatics and drug development visualizations.

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

I would like to be able to reorder the children of a div.

I have seen a previous example (Drag and drop cards - #2 by RenaudLN) that allow to drag&drop elements thanks to the Dragula js library.

However, while on the UI, the order of elements are changed, on the children property of the div, the children are not reordered.

I have adapted the code to adapt the children order within the javascript.

What I am still missing is the ability from the JS code to trigger the event “the children of this element has been changed, trigger the callbacks that depends on it”.

To make it clearer, here is an example:
app.py

import dash
import dash_html_components as html
from dash.dependencies import Input, Output, ClientsideFunction, State

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

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

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(component_id="btn", component_property="n_clicks"),
        Input(component_id="drag_container", component_property="children"),
    ],
)
def watch_children(nclicks, 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)

asset/script.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)
                });
                // How can I trigger an update on the children property
                // ???
            })
        }, 1)
        return window.dash_clientside.no_update
    }
}

assets/dargula.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,

I am trying to implement your solution, and it works fine when the children of the drag-container are set from the app-start.

However, I would like to update the children and then use the dragable feature.

This however, seem to be problematic, the new order is not saved and the dragging appears sloppy. (Not always responsive.)

edit: By new order I mean that the serverside order of the children is not linked to the displayed order AFTER the callback. Before callback with new drag-children it is linked.

Do you have any idea how to fix this.

thanks in advance
Henrik

These tweaks works for me.

This works with dynamic children in the drag-container and does trigger an update of children upon reordering:

App.py:

"""
App for testing drag and drop

"""


import dash
import dash_html_components as html
from dash.dependencies import Input, Output, ClientsideFunction, State


SERVER_PORT = 12327

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

app.layout = html.Div(
    id="main",
    children=[
        html.Button(id="btn", children="Refresh display for order of children"),
        html.Label(id="order"),
        html.Div(id="container"),
        html.Button(id="btn_children", children="new children")
    ],
)

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

)

@app.callback(
    Output('container', 'children'),
    Input('btn_children', 'n_clicks'),
    prevent_initial_call=True
)
def test(n_clicks):
    print("running")

    if (n_clicks % 2) == 0:
        range_ = range(5)
    else:
        range_ = range(5,10)

    a = html.Div(
        id="drag_container",
        className="container",
        children=[html.Button(id=f"child-{i}", children=f"child-{i}") for i in range_],
    ),
    return(a)





@app.callback(
    Output("order", "children"),
    [
        Input(component_id="btn", component_property="n_clicks"),
        Input(component_id="drag_container", component_property="children"),
    ],
    prevent_initial_call = True
)
def watch_children(nclicks, children):
    """Display on screen the order of children"""
    return ", ".join([comp["props"]["id"] for comp in children])


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

Script.js

if (!window.dash_clientside) {
    window.dash_clientside = {};
}
window.dash_clientside.clientside = {
    make_draggable: function (children, id) {
        setTimeout(function () {

            var drake = dragula({});
            var el = document.getElementById(id)
            var order = document.getElementById("order")
            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
                order.innerHTML = order_ids
            })
        }, 1)
        return window.dash_clientside.no_update
    }
}

1 Like

It appears that there is some desyncing problems with the above code.

You can observe this by printing the return in the callback for the order component.

sdementen,

Not sure if I understand you correctly, but if you want to fire the callback on element drop, you could always add a click event on your button so that the user is not having to deliberately click to see the new order. So your script.js would look like:

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)
            var btn = document.getElementById('btn')
            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
                btn.click();
                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)
                });
                // How can I trigger an update on the children property
                // ???
            })
        }, 1)
        return window.dash_clientside.no_update
    }
}

Your post has been super helpful, but I’m terrible at javascript and can’t for the life of me figure out how to alter it to output only the children in a particular element. So for example, if I have child elements in a wanted div and unwanted div, how would I output only the children in the wanted div AND in the correct order? Thanks!

Hello Guys,
does someone of you know how i can grab the order in my Python Code?

I want to work with these new order in my next step in python. The “.innerHTML” update only the browser not the ‘children’ element themselfes.

So if i try to grab the ‘children’ of the ‘order’ Label via a Button i get a “None”. Any ideas?

In dragging mode mouse wheel scrolling is not available. Is It possible to enable it and/or autoscroll while dragging?