Row Dragging to an External Dropzone

Row Dragging to an External DropZone is concerned with moving rows from the grid to different components within the same
application. When using row drag with an External DropZone, the data is moved or copied around using the grid events,
this is in contrast to standard Drag and Drop which uses browser events.

The Row Drag to an External DropZone uses the grid’s internal Managed Row Dragging system combined with row selection to
create a seamless data drag and drop experience.
The Grid API provide the following functions to add and remove an External DropZone to a Grid:

addRowDropZone (Function) Adds a drop zone outside the grid where rows can be dropped.
removeRowDropZone (Function) Removes an external DropZone added by addRowDropZone.

If you read the Managed Dragging section of the
Row Dragging documentation you probably noticed that when you sort, filter and rowGroup the Grid, the managed Row
Dragging stops working. The only exception to this rule is when you register external DropZones using addRowDropZone.
In this case, you will be able to drag from one container to another, but will not be able to drag the rows within the
grid.

Note that adding external DropZone uses the Grid API, this API can be used in JavaScript functions
through Clientside Callbacks.
Moreover, to make the Drag and Drop available at initialisation of the app, it is possible to use the Grid’s id as
Input for the clientside callback, which will be triggered at the Grid initialisation. But to be able to use the Grid
API
, the Grid must be fully initialized. To await its initialisation, it is possible to use the async function
dash_ag_grid.getApiAsync(gridId).

The async grid API dash_ag_grid.getApiAsync(gridId) is available
in <code>dash‑ag‑grid>=2.3.0<code>

Adding and Removing External Drop Zones

To allow dragging from the grid onto an outside element, or a different grid, call the addRowDropZone from the grid
API. This will result in making the passed element or grid a valid target when moving rows around. If you later wish to
remove that DropZone use the removeRowDropZone method from the grid API.

To customize the behavior of the External DropZone, addRowDropZone takes the following functions as parameters:

// Get the Grid API
const gridAPI = await dash_ag_grid.getApiAsync(gridId)

// Define the Drop Zone
const targetContainer = document.querySelector('.target-container');

const dropZoneParams = {
    // Function that returns the DropZone HTMLElement.
    getContainer: () => targetContainer,
    // Function that will be executed when the rowDrag enters the target.
    onDragEnter: params => {
        console.log('DropZone entered')
    },
    // Function that will be executed when the rowDrag leaves the target
    onDragLeave: params => {
        console.log('DropZone left')
    },
    // Function that will be executed when the rowDrag is dragged inside the target.
    // Note: this gets called multiple times.
    onDragging: params => {
        console.log('Dragging inside the DropZone')
    },
    // Function that will be executed when the rowDrag drops rows within the target.
    onDragStop: params => {
        // here we create an element for the target container
        const element = createElement(params.node.data);
        targetContainer.appendChild(element);
    }
}

// Register the Drop Zone
gridAPI.addRowDropZone(dropZoneParams);

// Deregister the Drop Zone when it is no longer required
gridAPI.removeRowDropZone(dropZoneParams);

In the example below, the following can be noted:

View the CSS classes used for this example

These CSS classes must be added to any *.css file in the assets folder.
See Loading CSS files for more information.

.grid-green-row, .dragged-tile.tile-green {
    background-color: #66c2a5 !important;
}

.grid-red-row, .dragged-tile.tile-red {
    background-color: #e78ac3 !important;
}

.grid-blue-row, .dragged-tile.tile-blue {
    background-color: #119dff !important;
}

.tile-container-header {
    border: 1px solid;
    height: 50px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 20px;
}

.tile-container {
    background-color: lightgray;
    height: 350px;
    overflow: auto;
    display: flex;
    align-items: flex-start;
    align-content: flex-start;
    flex-wrap: wrap;
}

.dragged-tile {
    width: calc(25% - 10px);
    margin: 5px;
    padding: 10px;
    border-radius: 5px;
    color: white;
}

View the Clientside Callbacks used for this example

These Clientside Callbacks must be added to any *.js file in the assets folder.
See Clientside Callbacks for more information.

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

window.dash_clientside.addDropZone = {
    dropZoneDiv: async function (gridId) {
        const gridAPI = await dash_ag_grid.getApiAsync(gridId)

        const tileContainer = document.querySelector('.tile-container');

        const dropZoneParams = {

            getContainer: () => {
                return tileContainer;
            },

            onDragStop: (params) => {
                Object.values(params.nodes).forEach(
                    function (node) {

                        let el = document.createElement('div');
                        el.classList.add('dragged-tile');
                        el.classList.add(`tile-${node.data.color.toLowerCase()}`);
                        el.innerHTML = '&lt;b&gt;ID: ' + node.data.id +
                            '&lt;b&gt;&lt;br&gt;Value1: ' + node.data.value1 +
                            '&lt;br&gt;Value2: ' + node.data.value2

                        tileContainer.appendChild(el);
                    }
                );
            },
        };

        gridAPI.addRowDropZone(dropZoneParams);

        return window.dash_clientside.no_update
    },
}
import dash_ag_grid as dag
from dash import Dash, html, Input, Output, ClientsideFunction, clientside_callback
import random

app = Dash(__name__)

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

columnDefs = [
    {'field': 'id', "checkboxSelection": True, "headerCheckboxSelection": True},
    {'field': 'color'},
    {'field': 'value1'},
    {'field': 'value2'},
]

app.layout = html.Div(
    [
        dag.AgGrid(
            id='row-dragging-external-dropzone-add-remove',
            rowData=row_data,
            columnDefs=columnDefs,
            defaultColDef={"filter": True},
            columnSize="sizeToFit",
            dashGridOptions={
                "rowDragManaged": True,
                "rowDragEntireRow": True,
                "rowDragMultiRow": True, "rowSelection": "multiple",
                "suppressMoveWhenRowDragging": True
            },
            rowClassRules={
                "grid-red-row": 'params.data.color == "Red"',
                "grid-green-row": 'params.data.color == "Green"',
                "grid-blue-row": 'params.data.color == "Blue"',
            },
        ),
        html.Div(
            [
                html.Div('Drop Zone', className="tile-container-header"),
                html.Div(className="tile-container")
            ],
            style={"marginTop": 15}
        )
    ]
)

clientside_callback(
    ClientsideFunction('addDropZone', 'dropZoneDiv'),
    Output('row-dragging-external-dropzone-add-remove', 'id'),
    Input('row-dragging-external-dropzone-add-remove', 'id')
)

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

Dragging Between Grids

It is possible to use a generic DropZone to Drag and Drop rows from one grid to another. However, this approach will
treat the target grid as a generic HTML Element and adding the rows should be handled by the onDragStop function.

The Grid API provide the getRowDropZoneParams function that can be used with the Target Grid that can then be provided
to the addRowDropZone function of the Source Grid, allowing rows to be dragged from the Source Grid and dropped at a
specific index on the Target Grid.

The Drag and Drop behavior can be customized by providing any of the
functions onDragEnter, onDragLeave, onDragging, onDragStop to the getRowDropZoneParams function.
See Grid to Grid Complex Example

getRowDropZoneParams (Function) Returns the RowDropZoneParams to be used by another grid’s addRowDropZone method.

Grid to Grid Simple Example

The example below uses the default getRowDropZoneParams, the following can be noted:

View the CSS classes used for this example

These CSS classes must be added to any *.css file in the assets folder.
See Loading CSS files for more
information.

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

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

.row-dragging-grid-to-grid-container {
    display: flex;
    align-items: center;
    column-gap: 20px;
}

View the Clientside Callbacks used for this example

These Clientside Callbacks must be added to any *.js file in the assets folder.
See Clientside Callbacks for more information.

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

window.dash_clientside.addDropZone = {
    dropZoneGrid2GridSimple: async function (gridIdLeft, gridIdRight) {
        // Get the grids APIs
        const gridLeftAPI = await dash_ag_grid.getApiAsync(gridIdLeft);
        const gridRightAPI = await dash_ag_grid.getApiAsync(gridIdRight);

        // Get the dropzones parameters from each grid
        const gridLeftDropZone = gridLeftAPI.getRowDropZoneParams();
        const gridRightDropZone = gridRightAPI.getRowDropZoneParams();

        // Add RIGHT grid as dropzone of LEFT grid
        gridLeftAPI.addRowDropZone(gridRightDropZone);
        // Add LEFT grid as dropzone of RIGHT grid
        gridRightAPI.addRowDropZone(gridLeftDropZone);

        return window.dash_clientside.no_update
    },
}
import dash_ag_grid as dag
from dash import Dash, html, Input, Output, ClientsideFunction, State, clientside_callback, callback
import random

app = Dash(__name__)

left_data = [
    {
        'id': i + 100,
        'color': 'Blue',
        'value1': random.randint(1, 100),
        'value2': random.randint(1, 100),
    } for i in range(5)
]

right_data = [
    {
        'id': i + 200,
        'color': 'Green',
        'value1': random.randint(1, 100),
        'value2': random.randint(1, 100),
    } for i  in range(5)
]


def init_grid(side):
    columnDefs = [
        {'field': 'id', "checkboxSelection": True, "headerCheckboxSelection": True},
        {'field': 'color'},
        {'field': 'value1'},
        {'field': 'value2'},
    ]

    return dag.AgGrid(
        id=f'row-dragging-grid2grid-simple-{side}',
        rowData=left_data if side == 'left' else right_data,
        columnDefs=columnDefs,
        defaultColDef={'resizable': True},
        columnSize="sizeToFit",
        dashGridOptions={
            "rowDragManaged": True,
            "rowDragEntireRow": True,
            "rowDragMultiRow": True, "rowSelection": "multiple",
            "suppressMoveWhenRowDragging": True
        },
        rowClassRules={
            "grid-green-row": 'params.data.color == "Green"',
            "grid-blue-row": 'params.data.color == "Blue"',
        },
        getRowId="params.data.id",
    )


app.layout = html.Div(
    [
        html.Button('Reset', id='btn-row-dragging-grid2grid-simple-reset'),
        html.Div(
            [
                init_grid('left'),
                init_grid('right'),
            ], className='row-dragging-grid-to-grid-container',
        )
    ]
)

# Set Grids as dropzone for each other
clientside_callback(
    ClientsideFunction('addDropZone', 'dropZoneGrid2GridSimple'),
    Output('row-dragging-grid2grid-simple-left', 'id'),
    Input('row-dragging-grid2grid-simple-left', 'id'),
    State('row-dragging-grid2grid-simple-right', 'id'),
)


@callback(
    Output("row-dragging-grid2grid-simple-left", "rowData"),
    Output("row-dragging-grid2grid-simple-right", "rowData"),
    Input("btn-row-dragging-grid2grid-simple-reset", "n_clicks"),
    prevent_initial_call=True,
)
def reset_rows(_):
    return left_data, right_data


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

Grid to Grid Complex Example

This example shows a more complex use case, using a customized Drag and Drop between Grids and a Bin as a DropZone to
remove the dropped rows. The following can be noted:

View the CSS classes used for this example

These CSS classes must be added to any *.css file in the assets folder.
See Loading CSS files for more
information.

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

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

.row-dragging-grid-to-grid-container {
    display: flex;
    align-items: center;
    column-gap: 20px;
}

#div-row-dragging-grid2grid-complex-bin {
    transition: transform 500ms;
}

View the Clientside Callbacks used for this example

These Clientside Callbacks must be added to any *.js file in the assets folder.
See Clientside Callbacks for more information.

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

window.dash_clientside.addDropZone = {
    dropZoneGrid2GridComplex: async function (dropOption, gridIdLeft, gridIdRight) {

        // Get the grids APIs
        const gridLeftAPI = await dash_ag_grid.getApiAsync(gridIdLeft);
        const gridRightAPI = await dash_ag_grid.getApiAsync(gridIdRight);

        const addBinDropZone = sourceGridAPI => {

            const binContainer = document.querySelector('#div-row-dragging-grid2grid-complex-bin');

            const binDropZoneParams = {
                getContainer: () => binContainer,

                onDragEnter: params => {
                    binContainer.style.color = '#e78ac3';
                    binContainer.style.transform = 'scale(1.5)';
                },

                onDragLeave: params => {
                    binContainer.style.color = null;
                    binContainer.style.transform = 'scale(1)';
                },

                onDragStop: params => {
                    binContainer.style.color = null;
                    binContainer.style.transform = 'scale(1)';

                    // Remove dragged rows from the Source Grid
                    sourceGridAPI.applyTransaction({
                        remove: params.nodes.map(node => node.data)
                    });
                },
            }

            sourceGridAPI.addRowDropZone(binDropZoneParams);

        };

        addBinDropZone(gridLeftAPI)
        addBinDropZone(gridRightAPI)

        const addGridDropZone = (sourceGridAPI, targetGridAPI) => {

            const gridDropZoneParams = {
                onDragStop: params => {

                    if (dropOption === 'move') {
                        // Remove dragged rows from the Source Grid
                        sourceGridAPI.applyTransaction({
                            remove: params.nodes.map(node => node.data)
                        });
                    } else if (dropOption === 'deselect') {
                        // Only deselect all rows
                        sourceGridAPI.deselectAll();
                    }
                },
            }

            const gridDropZone = targetGridAPI.getRowDropZoneParams(gridDropZoneParams);
            // Remove existing gridDropZone before adding the updated one depending on the dropOption
            sourceGridAPI.removeRowDropZone(gridDropZone);
            sourceGridAPI.addRowDropZone(gridDropZone);

        };

        addGridDropZone(gridLeftAPI, gridRightAPI)
        addGridDropZone(gridRightAPI, gridLeftAPI)

        return window.dash_clientside.no_update
    },
}
import dash_ag_grid as dag
from dash import Dash, html, dcc, Input, Output, ClientsideFunction, State, clientside_callback, callback
from dash_iconify import DashIconify
import random

app = Dash(__name__)

left_data = [
    {
        'id': i + 100,
        'color': 'Blue',
        'value1': random.randint(1, 100),
        'value2': random.randint(1, 100),
    } for i in range(5)
]

right_data = [
    {
        'id': i + 200,
        'color': 'Green',
        'value1': random.randint(1, 100),
        'value2': random.randint(1, 100),
    } for i in range(5)
]


def init_grid(side):
    columnDefs = [
        {'field': 'id', "checkboxSelection": True, "headerCheckboxSelection": True},
        {'field': 'color'},
        {'field': 'value1'},
        {'field': 'value2'},
    ]

    return dag.AgGrid(
        id=f'row-dragging-grid2grid-complex-{side}',
        rowData=left_data if side == 'left' else right_data,
        columnDefs=columnDefs,
        defaultColDef={'resizable': True},
        columnSize="sizeToFit",
        dashGridOptions={
            "rowDragManaged": True,
            "rowDragEntireRow": True,
            "rowDragMultiRow": True, "rowSelection": "multiple",
            "suppressMoveWhenRowDragging": True
        },
        rowClassRules={
            "grid-green-row": 'params.data.color == "Green"',
            "grid-blue-row": 'params.data.color == "Blue"',
        },
        getRowId="params.data.id",
    )


app.layout = html.Div(
    [
        html.Button('Reset', id='btn-row-dragging-grid2grid-complex-reset'),
        dcc.RadioItems(
            id='radio-row-dragging-grid2grid-complex-option',
            options={
                'move': 'Move',
                'deselect': 'Copy and Deselect',
                'none': 'Copy and Keep Selected'
            },
            value='move', inline=True, style={'margin': 10}
        ),
        html.Div(
            [
                init_grid('left'),
                html.Div(
                    DashIconify(icon="fa6-regular:trash-can", width=30),
                    id='div-row-dragging-grid2grid-complex-bin'
                ),
                init_grid('right'),
            ], className='row-dragging-grid-to-grid-container',
        )
    ]
)

clientside_callback(
    ClientsideFunction('addDropZone', 'dropZoneGrid2GridComplex'),
    Output('row-dragging-grid2grid-complex-left', 'id'),
    Input('radio-row-dragging-grid2grid-complex-option', 'value'),
    State('row-dragging-grid2grid-complex-left', 'id'),
    State('row-dragging-grid2grid-complex-right', 'id'),
)


@callback(
    Output("row-dragging-grid2grid-complex-left", "rowData"),
    Output("row-dragging-grid2grid-complex-right", "rowData"),
    Input("btn-row-dragging-grid2grid-complex-reset", "n_clicks"),
    prevent_initial_call=True,
)
def reset_rows(_):
    return left_data, right_data


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