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)
4 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

Here is the updated version for 2.2.0:

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

            if (target == 'grid2') {
                dash_ag_grid.getApi('grid2').applyTransactionAsync({'add': [data]})
            } else {
                dash_ag_grid.getApi('grid1').applyTransactionAsync({'add': [data]})
            }

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

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

Hi guys !

I’m working on the row dragging docs, there are some convenient Grid API functions for drag and drop I think you can be interested !

.addRowDropZone() to add any element as a drop zone
.getRowDropZoneParams() that get the parameters from another grid that can be then used as drop zone

So the callback to add drag and drop between grids can be as simple as:

const gridDropZone = dash_ag_grid.getApi(targetGridId).getRowDropZoneParams();
dash_ag_grid.getApi(sourceGridId).addRowDropZone(gridDropZone);

Here is a simple example, that uses multi row select/dragging and entire row as dragger:

Code
import dash_ag_grid as dag
from dash import Dash, html, Input, Output, State
import random

app = Dash(__name__)


def init_grid(side):
    row_data = [
        {
            'id': i + (100 if side == 'left' else 200),
            'color': color,
            'value1': random.randint(1, 100),
            'value2': random.randint(1, 100),
        } for i, color in enumerate(['Red', 'Green', 'Blue', 'Red', 'Green'])
    ]

    columnDefs = [
        {'field': 'id'},
        {'field': 'color'},
        {'field': 'value1'},
        {'field': 'value2'},
    ]

    return dag.AgGrid(
        id=f'my-grid-{side}',
        rowData=row_data,
        columnDefs=columnDefs,
        defaultColDef={"sortable": True, "filter": True, 'resizable': True},
        columnSize="sizeToFit",
        dashGridOptions={
            "rowDragManaged": True,
            "rowDragEntireRow": True,
            "rowDragMultiRow": True, "rowSelection": "multiple",
            "suppressMoveWhenRowDragging": True,
        },
        rowClassRules={
            "red-row": 'params.data.color == "Red"',
            "green-row": 'params.data.color == "Green"',
            "blue-row": 'params.data.color == "Blue"',
        },
        getRowId="params.data.id",
    )


app.layout = html.Div(
    [
        init_grid('left'),
        init_grid('right'),
    ], className='container',
)

# Set Right grid as drop zone for Left Grid
app.clientside_callback(
    """
    function (sourceGridId, targetGridId) {
        setTimeout(() => {
            const gridDropZone = dash_ag_grid.getApi(targetGridId).getRowDropZoneParams();
            dash_ag_grid.getApi(sourceGridId).addRowDropZone(gridDropZone);
        }, 500)
        return window.dash_clientside.no_update
    }
    """,
    Output('my-grid-left', 'id'),
    Input('my-grid-left', 'id'),
    State('my-grid-right', 'id'),
)

# Set Left grid as drop zone for Right Grid
app.clientside_callback(
    """
    function (sourceGridId, targetGridId) {
        setTimeout(() => {
            const gridDropZone = dash_ag_grid.getApi(targetGridId).getRowDropZoneParams();
            dash_ag_grid.getApi(sourceGridId).addRowDropZone(gridDropZone);
        }, 500)
        return window.dash_clientside.no_update
    }
    """,
    Output('my-grid-right', 'id'),
    Input('my-grid-right', 'id'),
    State('my-grid-left', 'id')
)

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

With some styling
.container {
    height: 400px;
    width: 800px;
    display: flex;
    flex-direction: row;
    align-items: center;
    column-gap: 20px;
}

.green-row {
    background-color: #66c2a5 !important;
}

.red-row {
    background-color: #e78ac3 !important;
}

.blue-row {
    background-color: #119dff !important;
}

This example simply copy dragged/dropped rows without duplicates, but obviously this is more customizable !

More interesting examples will come soon in the docs ! :sunglasses:

2 Likes

Yes, not easy to debug in a string ! But there is a way to use functions from a .js file :grin:
Like I use for the more complex drag and drop examples that will come in the docs

app.clientside_callback(
    ClientsideFunction('addDropZone', 'dropGrid'),
    Output('my-grid-right', 'id'),
    Input('my-grid-right', 'id'),
    State('my-grid-left', 'id')
)

and in file in /assets/any_name.js:

window.dash_clientside = window.dash_clientside || {};

window.dash_clientside.addDropZone = {
  dropGrid: function (sourceGridId, targetGridId) {
  
  }
}

And voilà !

3 Likes