Python Callback from JS on dragdrop (with(out) dash-extensions)

Hi
I managed to create a dragdrop from/to containers using dragula.js

But how can I get a callback in Python when elements are dragged, dropped etc .
Eg to get the added/removed elements in the containers.

I’m trying it by using dash-extensions EventListener (sounds like what I need … ?)
When inspecting the app in the webbrowser I see a few js eventlisteners.
The events ‘click’, 'mouseup, ‘mousedown’ work as expected.
But the ‘touchend’, "touchstart events do not seem to work…

Also Dragula itself specifies different eventnames eg. ‘drop’, ‘cancel’, ‘remove’, …

Kinda lost here…
Note that ‘click’ events work fine (uncomment)

Python

import dash
from dash import dcc
import dash_bootstrap_components as dbc
from dash_extensions.enrich import DashProxy, html, Input, Output, State, ClientsideFunction, NoOutputTransform
from dash_extensions import EventListener

app = DashProxy(transforms=[NoOutputTransform()])
app.config.external_stylesheets = ["https://epsi95.github.io/dash-draggable-css-scipt/dragula.css", dbc.themes.BOOTSTRAP]
app.config.external_scripts = ["https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.2/dragula.min.js",
                               "https://epsi95.github.io/dash-draggable-css-scipt/script.js"]

cols = ['aaa','bbb','ccc','ddd','eee','fff']

events = [
    #{"event": "click"   , "props": ["srcElement.id"]},
    {"event": "touchend", "props": ["srcElement.id"]},
    ]

app.layout = dbc.Container(
    [
        EventListener(
            html.Div([
                dbc.Row([
                    dbc.Col([
                        html.Div([
                            html.Div([
                                html.P(
                                        x,
                                        style={
                                            'text-indent':'28px'
                                            }
                                        )
                                    ],
                                     id = x,
                                     style={
                                         'backgroundColor':'rgb(180,180,180)',
                                         'border': '1px solid black',
                                         'height':'28px'
                                         }
                                    )
                                    for x in cols
                                    ],
                                 id="drag_container1",
                                 className="container",
                                 style ={
                                        'backgroundColor' : 'rgb(100,100,100)',
                                        'margin': 0,
                                        'padding': 0,
                                        'height' : '300px',
                                        'overflow-y':'auto',
                                        }
                                    )
                        ]),
                    dbc.Col([
                        html.Div(
                            [],
                            id="drag_container2",
                            className="container",
                            style ={
                                'backgroundColor' : 'rgb(100,100,100)',
                                'margin': 0,
                                'padding': 0,
                                'height' : '300px',
                                'overflow-y':'auto',
                                }
                            )
                        ]),
                    ]),
                ],
                     id="drag_container0",
                     className="container",
                     style ={
                         'width':'80%'
                         }
                     ),
            events=events,
            logging=True,
            id="el"),
        ],
        className="dbc",
        fluid=True,
        )

app.clientside_callback(
    ClientsideFunction(namespace="clientside", function_name="make_draggable_mre"),
    Output("drag_container0", "data-drag"),
    [Input("drag_container1", "id"),Input("drag_container2", "id")]
)

@app.callback(Input("el", "n_events"), 
              State("el", "event"))
def get_event(n_events, e):
    print('{}   {}'.format(n_events, e))
    if e is None:
        return dash.no_update
    return f"{e} \n (number of clicks is {n_events})"



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

assets/script.js

if (!window.dash_clientside) {
    window.dash_clientside = {};
}
window.dash_clientside.clientside = {
    make_draggable_mre: function () {
        let args = Array.from(arguments);
        var containers = [];
        setTimeout(function () {
            for (i = 0; i < args.length; i++) {
                containers[i] = document.getElementById(args[i]);
            }
            dragula(containers);
        }, 1)
        return window.dash_clientside.no_update
    }
}

Is there any way to catch the js drop-events in python?

1 Like

Hello @popo,

You could try listening to the children of the containers and if they changed.

Not sure if this will work for reordering, but I think it might work for adding and removing.

Hi
After doing some sniffing around I got it working (thx to anyone who contributed):
The callback returns dragdrop info such as : element dragged, source/target container ids and their ordered children.
I kept within the scope of a mre but this method can easily be upgraded to support more dragula info if needed.

Python

import dash
from dash import dcc
import dash_bootstrap_components as dbc
from dash_extensions.enrich import DashProxy, html, Input, Output, State, ClientsideFunction, NoOutputTransform
import json
from pprint import pprint

app = DashProxy(transforms=[NoOutputTransform()])
app.config.external_stylesheets = ["https://epsi95.github.io/dash-draggable-css-scipt/dragula.css", dbc.themes.BOOTSTRAP]
app.config.external_scripts = ["https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.2/dragula.min.js",
                               "https://epsi95.github.io/dash-draggable-css-scipt/script.js"]

rows = ['aaa','bbb','ccc','ddd','eee','fff']

app.layout = dbc.Container(
    [
            html.Div([
                dbc.Input(type='text', id='client_event_receiver', placeholder='', style={'visibility': 'hidden'}),
                dbc.Row([
                    dbc.Col([
                        html.Div([
                            html.Div([
                                html.P(
                                        x,
                                        style={
                                            'text-indent':'28px'
                                            }
                                        )
                                    ],
                                     id = x,
                                     style={
                                         'backgroundColor':'rgb(180,180,180)',
                                         'border': '1px solid black',
                                         'height':'28px'
                                         }
                                    )
                                    for x in rows
                                    ],
                                 id="drag_container1",
                                 className="container",
                                 style ={
                                        'backgroundColor' : 'rgb(100,100,100)',
                                        'margin': 0,
                                        'padding': 0,
                                        'height' : '300px',
                                        'overflow-y':'auto',
                                        }
                                    ),
                        ]),
                    dbc.Col([
                        html.Div(
                            [],
                            id="drag_container2",
                            className="container",
                            style ={
                                'backgroundColor' : 'rgb(100,100,100)',
                                'margin': 0,
                                'padding': 0,
                                'height' : '300px',
                                'overflow-y':'auto',
                                }
                            )
                    ]),
                ]
                        )],
                     id="drag_container",
                     className="container",
                     style ={
                         'width':'80%'
                         }
                     ),
        ],
        className="dbc",
        fluid=True,
        )

app.clientside_callback(
    ClientsideFunction(namespace="clientside", function_name="make_draggable_mre"),
    Output("drag_container", "data-drag"),
    [Input("drag_container1", "id"),Input("drag_container2", "id")
     ]
)
@app.callback(Input('client_event_receiver', 'value'))
def get_dragdrop_info(json_str):
    if json_str:
        value = json.loads(json_str)
        print('-'*100)
        pprint(value)


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

assets/script.js

if (!window.dash_clientside) {
    window.dash_clientside = {};
    var __rendezvouz_setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;

}
window.dash_clientside.clientside = {
    make_draggable_mre: function () {
        let args = Array.from(arguments);
        var containers = [];
        setTimeout(function () {
            for (i = 0; i < args.length; i++) {
                containers[i] = document.getElementById(args[i]);
            }
            dragul = dragula(containers);
            dragul.on("drop", function (el, target, source, sibling) {
                var result = {
                    'element': el.id,
                    'target_id': target.id,
                    'target_children': Array.from(target.children).map(function (child) {return child.id;})
                    }              
                if (source.id != target.id) {
                    result['source_id'] = source.id;
                    result['source_children'] = Array.from(source.children).map(function (child) {return child.id;});
				}
                var client_event_receiver = document.getElementById("client_event_receiver");
                __rendezvouz_setter.call(client_event_receiver, JSON.stringify(result));
                var client_event = new Event('input', { bubbles: true });
                client_event_receiver.dispatchEvent(client_event);
                    
            })
        }, 1)
        return window.dash_clientside.no_update
    }
}

Prints the following when dragging the rows around:

Untitled

Note that only the targets are returned when reordering inside the same container

As a base, I used:

var __rendezvouz_setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;

to trigger events on a dummy dbc.Input object.

Guess it has something to do with React?
Although I can’t foresee many drawbacks to this method yet, I don’t have the necessary expertise to determine if this is good practice or not. I do not completely understand what I did, so if anyone can clarify, It would be nice receive some further explanations.

For as far as I know, dash-extensions doesn’t provide a way to return a callback from js. (please, correct me if I’m wrong)
I encountered quite a few questions similar to this one.
It would be nice if dash-extensions would implement something similar to the above for communicating client events to the server… in that that lovely, clean, streamlined fashion. Big up to the dash-extensions devs! :wink:

2 Likes

That would do it, instead of an input you could use a dcc.store in memory, it can store all types of things as well.

An input in the dom, you can see, a store in memory, not so much.

You could also open it up to some pattern matching to get really fancy. But I definitely like this.

The trigger you are using is to bridge the gap between the server and client, you have to set the value via react, and not JavaScript regular because it just doesn’t play nice.

@jinnyzor
Thanks for this quick response.

Yes, dcc.store looks mighty fine, especially for it’s ability to be used in ‘memory’, ‘session’, local’ mode.

But…

In Python, I replaced the dbc.Input with a dcc.Store.

...

#dbc.Input(type='text', id='client_event_receiver', placeholder='', style={'visibility': 'hidden'}),
dcc.Store(id='client_event_receiver', storage_type ='memory'),

...

#@app.callback(Input('client_event_receiver', 'value'))
@app.callback(Input('client_event_receiver', 'data'))

...

In the assets/script.js
Set the dcc.Store ‘data’ property instead of dbc.Input ‘value’


//var __rendezvouz_setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
var __rendezvouz_setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "data").set;

The javascript crashes:

The problem is that id=‘client_event_receiver’ cannot be found in the HTML
Why? Where is the dcc.Store in the HTML

var client_event_receiver = document.getElementById("client_event_receiver"); // RETURNS null

Untitled

Except from the solution above, I have no experience with React.
Would you care to shed some light please?

Stores are not found in the actual html, but in localStorage or sessionStorage of the window(I think).

Thanks @jinnyzor
dcc_Stores can be found in localStorage and/or sessionStorage, depending on the Dcc.Store storage_type mode.

Couldn’t locate the dcc.Store with storage_type ‘memory’ though.
I tried caches, CacheStorage, … no can do…

Yeah. Memory is not available through client side. Local and Session you can however.

Ok,

But when trying to use dcc.Store, which is most probably the better solution, my total noobness on react stuff bites me up the … again. Eg. How to do that ‘__rendezvouz_setter’ thingy with sessionStorage and/or localStorage.

Guess I’ll just give up…
I will stick to what I know and use the dbc.Input or dcc.Input (with persistence and persistence_type) to emulate the dcc.Store functionality. As long as I stick to json strings, I’ll be able to communicate everything I need from the client to the server.

Still hope that the dash-extensions dudes will provide a proper, streamlined, safe way to fill this ‘important’ gap in dash.
Or maybe, I just lack the knowledge.