Dash AG Grid: How to Drag Drop between grids

Hi Everyone

I would like to know how to use the drag drop functionality of Python Dash AG Grid to drag a row from one grid to another grid. Is this possible? And how would the required properties and callback need to be configured?

An example would be much appreciated.

Thank you.

Hello @JJP,

Welcome to the community!

I don’t think that this can happen yet. I think you need to have some other functionality yet.

I believe that according to the docs, the div itself needs to have the drag enter events applied to it and not the grid.

Hi @jinnyzor ,

Thanks for you quick response.

Is it my correct understanding that the AG-Grid features are limited when using them via Dash?
As it seems that drag and drop is supported ( React Data Grid: Drag & Drop ) but not (yet) via Dash.

Some background information: I would like to assign tasks to persons, such that the user can drag a task from the table with tasks and drop it on a person in the table listing persons.
Does anyone have a suggestion to support such an action in a user friendly way, other than drag-drop?

Thanks in advance

Correct, the grid gives this as an example for between two grids.

With this explanation:

On initial analysis, consideration was given to exposing callbacks or firing events in the grid for the drop zone relevant events e.g. onDragEnter , onDragExit etc. However this did not add any additional value given that the developer can easily add such event listeners to the grid div directly.

Now, since it is not as easily done in Dash, for obvious reasons. We have to design our own way.

Technically, this isn’t directly supported but just an example of how to utilize the tools provided by the grid to be able to do such things.

You can personally add event listeners directly to the outside div, by using a clientside callback. The main thing is that the grid needs to get rowManagedDrag: True via the dashGridOptions.

HI @JJP

A different solution might be the TransferList component in the Dash Mantine Components library. It’s not drag and drop, but it might provide the functionality you are looking for.

I’d be very interested in seeing an example of this event listener in clientside callback. There’s lots of functionality around dragging and other events (for example row dragging with tree data) that would be great to be able to use

I believe that you can drag tree data innately. :slight_smile:

I can possibly work on an example to drag rows between grids, in the current state.

@alistair.welch,

Here is a working example of how to do this without any additional options from the grid:

import dash_ag_grid as dag
import pandas as pd
from dash import Dash, html, Output, Input, dcc


app = Dash(__name__)

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

columnDefs = [
    {"field": "country", "dndSource": True},
    {"field": "year"},
    {"field": "sport"},
    {"field": "gold"},
    {"field": "silver"},
    {"field": "bronze"},
]

app.layout = html.Div(
    [
        dag.AgGrid(
            columnDefs=columnDefs,
            rowData=df.to_dict("records")[:5],
            columnSize="sizeToFit",
            defaultColDef={"resizable": True, "sortable": True, "filter": True},
            dashGridOptions={"rowDragManaged": True, "animateRows": True},
            style={'width': '45%'},
            id='grid1'
        ),
        dag.AgGrid(
            columnDefs=columnDefs,
            rowData=df.to_dict("records")[6:10],
            columnSize="sizeToFit",
            defaultColDef={"resizable": True, "sortable": True, "filter": True},
            dashGridOptions={"rowDragManaged": True},
            style={'width': '45%'},
            id='grid2'
        ),
        html.Button(id='drop1', style={'display':'none'}, n_clicks=0),
        html.Button(id='drop2', style={'display':'none'}, n_clicks=0),
        dcc.Store(id='dropStore', storage_type='session')
    ],
    style={"margin": 20, "display": "flex"},
)

## enable drag-n-drop
app.clientside_callback(
    """
    function (p) {
        const mouseClickEvents = ['mousedown', 'click', 'mouseup'];
        function simulateMouseClick(element, args) {
            mouseClickEvents.forEach((mouseEventType) =>
                element.dispatchEvent(
                    new MouseEvent(mouseEventType, {
                        view: window,
                        bubbles: true,
                        cancelable: true,
                        buttons: 1,
                        target: element,
                        ...args,
                    })
                )
            );
            mouseClickEvents.forEach((mouseEventType) =>
                element.dispatchEvent(
                    new PointerEvent(mouseEventType, {
                        view: window,
                        bubbles: true,
                        cancelable: true,
                        buttons: 1,
                        target: element,
                        ...args,
                    })
                )
            );
        }
        var gridOrigin;
        var gridDragOver = (event) => {
            const dragSupported = event.dataTransfer.types.length;

            if (dragSupported) {
              event.dataTransfer.dropEffect = 'copy';
              event.preventDefault();
            }
          };
          
        var gridDragStart = (origin, event) => {
            gridOrigin = origin
        }
          
        var gridDrop = (target, event) => {
            event.preventDefault();
        
            const jsonData = event.dataTransfer.getData('application/json');
            const data = JSON.parse(jsonData);
        
            // if data missing or the drop target is the same as the origin, do nothing
            if (!data || target == gridOrigin) {
              return;
            }
            
            // store data for use in transfer
            sessionStorage.setItem('dropStore', jsonData)

            if (target == 'grid2') {
                simulateMouseClick(document.querySelector('#drop2'))
            } else {
                simulateMouseClick(document.querySelector('#drop1'))
            }
            
          };
        setTimeout(() => {
        document.querySelector('#grid1').addEventListener('dragstart', (event)=>gridDragStart('grid1', event))
        document.querySelector('#grid2').addEventListener('dragstart', (event)=>gridDragStart('grid2', event))
        
        document.querySelector('#grid1').addEventListener('dragover', gridDragOver)
        document.querySelector('#grid2').addEventListener('dragover', gridDragOver)
        document.querySelector('#grid2').addEventListener('drop', (event)=>gridDrop('grid2', event))
        document.querySelector('#grid1').addEventListener('drop', (event)=>gridDrop('grid1', event))
        }, 500)
        return window.dash_clientside.no_update
    }
    """,
    Output('grid2', 'id'),
    Input('grid2', 'id')
)

app.clientside_callback(
    """
    function (n) {
        data = JSON.parse(sessionStorage.getItem('dropStore'))
        
        //remove stored session data
        sessionStorage.setItem('dropStore', null)
        return {'add': [data]}
    }
    """,
    Output('grid2', 'rowTransaction'),
    Input('drop2', 'n_clicks'),
    prevent_initial_call=True
)

app.clientside_callback(
    """
    function (n) {
        data = JSON.parse(sessionStorage.getItem('dropStore'))
        
        //remove stored session data
        sessionStorage.setItem('dropStore', null)
        return {'add': [data]}
    }
    """,
    Output('grid1', 'rowTransaction'),
    Input('drop1', 'n_clicks'),
    prevent_initial_call=True
)

if __name__ == "__main__":
    app.run_server(debug=True, port=1234)
3 Likes

Ah I’ll do some more tests with the tree data dragging then.

Incredibly strong stuff in that example again, unlocking all the grid events so thank you!

Do you know, is there a plan to make the grid events more accessible? I did have something working on a custom component where you could define event code in strings in the Python, there was also a placeholder event info prop so you could pass arbitrary data back from a grid event into a Dash prop and hence trigger a callback

Not sure if I follow 100%.

I think we are trying to limit the amount of events to what the normal scope of the Grid will be. If we tack on more events, there could be some degradation in functionality. :slight_smile:

Oh yeah the performance is awful if you register all the events. This enabled defining your js functions and passing them via gridoptions and that would register them, so you only register the ones you need.

I believe streamlit ag grid did it and I took inspiration from that.

Something similar to the jsfuncs.js file that is currently used for functions in the coldefs was the idea.

I’ll do a separate post or github fork to show what I did if there’s interest

Keep the tricks coming …

2 Likes

As much fun as it is to pass these as JS strings, this could cause some issues, haha.

Might be able to do some other things though.

Oh yeah 100% haha but hopefully it can be achieved in a safer way.

You’re clearly already doing something to make those funcs from the js file become available to the grid and I haven’t yet tried to combine that idea with the events defined via gridoptions idea but I’m planning to

2 Likes