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 & 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.
Adds a drop zone outside of the grid where rows can be dropped. |
Removes an external drop zone added by addRowDropZone. |
If you read the Managed Row 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 drop zones 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.
Adding and Removing Row Drop Targets Copy Link
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 drop zone use the removeRowDropZone method from the grid API.
// define drop zone
const targetContainer = document.querySelector('.target-container');
const dropZoneParams = {
getContainer: () => targetContainer,
onDragStop: params => {
// here we create an element for the target container
const element = createElement(params.node.data);
targetContainer.appendChild(element);
}
};
// register drop zone with grid
gridApi.addRowDropZone(dropZoneParams);
// deregister the drop zone when no longer required
gridApi.removeRowDropZone(dropZoneParams);In the example below, note the following:
- You can move rows inside the grid.
- You can move rows to the container on the right hand side.
- Toggle the checkbox to enable or disable suppressMoveWhenRowDragging
"use client";
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./style.css";
import {
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GridApi,
GridOptions,
GridReadyEvent,
ModuleRegistry,
RowClassRules,
RowDragModule,
RowDropZoneParams,
RowStyleModule,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
const modules = [
TextFilterModule,
RowDragModule,
RowStyleModule,
ClientSideRowModelModule,
];
let rowIdSequence = 100;
function addCheckboxListener(params: GridReadyEvent) {
const checkbox = document.querySelector("input[type=checkbox]")! as any;
checkbox.addEventListener("change", () => {
params.api.setGridOption("suppressMoveWhenRowDragging", checkbox.checked);
});
}
function createRowData() {
const data: any[] = [];
[
"Red",
"Green",
"Blue",
"Red",
"Green",
"Blue",
"Red",
"Green",
"Blue",
].forEach((color) => {
const newDataItem = {
id: rowIdSequence++,
color: color,
value1: Math.floor(window.agRandom() * 100),
value2: Math.floor(window.agRandom() * 100),
};
data.push(newDataItem);
});
return data;
}
function createTile(data: any) {
const el = document.createElement("div");
el.classList.add("tile");
el.classList.add(data.color.toLowerCase());
el.innerHTML =
'<div class="id">' +
data.id +
"</div>" +
'<div class="value">' +
data.value1 +
"</div>" +
'<div class="value">' +
data.value2 +
"</div>";
return el;
}
function addDropZones(params: GridReadyEvent) {
const tileContainer = document.querySelector(".tile-container") as any;
const dropZone: RowDropZoneParams = {
getContainer: () => {
return tileContainer as any;
},
onDragStop: (params) => {
const tile = createTile(params.node.data);
tileContainer.appendChild(tile);
},
};
params.api.addRowDropZone(dropZone);
}
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<any[]>(createRowData());
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "id", rowDrag: true },
{ field: "color" },
{ field: "value1" },
{ field: "value2" },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
filter: true,
flex: 1,
};
}, []);
const rowClassRules = useMemo<RowClassRules>(() => {
return {
"red-row": 'data.color == "Red"',
"green-row": 'data.color == "Green"',
"blue-row": 'data.color == "Blue"',
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
addDropZones(params);
addCheckboxListener(params);
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div className="toolbar">
<label>
<input type="checkbox" /> Enable suppressMoveWhenRowDragging
</label>
</div>
<div className="drop-containers">
<div className="grid-wrapper">
<div style={gridStyle}>
<AgGridReact
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
rowClassRules={rowClassRules}
rowDragManaged={true}
onGridReady={onGridReady}
/>
</div>
</div>
<div className="drop-col">
<span id="eDropTarget" className="drop-target">
==> Drop to here
</span>
<div className="tile-container"></div>
</div>
</div>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
body {
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 13px;
}
.example-wrapper {
display: flex;
height: 100%;
flex-direction: column;
}
.drop-containers {
display: flex;
flex: 1 1 auto;
flex-direction: row;
position: relative;
overflow: hidden;
}
.toolbar {
height: 30px;
}
.grid-wrapper {
flex: 2 2 0px;
min-width: 0;
}
.drop-col {
padding-left: 10px;
box-sizing: border-box;
flex: 1 1 0px;
display: flex;
flex-direction: column;
}
.drop-target,
.tile {
color: white;
}
.drop-target {
border: 1px solid #888;
background-color: #99999966;
padding: 10px;
}
.tile-container {
box-sizing: border-box;
border: 1px solid #888;
background-color: #99999944;
overflow: auto;
display: flex;
align-items: flex-start;
align-content: flex-start;
flex-wrap: wrap;
flex: 1 1 auto;
}
.tile {
width: calc(50% - 30px);
margin: 5px;
padding: 10px;
border-radius: 5px;
}
.red-row {
background-color: #cc333344;
}
.green-row {
background-color: #33cc3344;
}
.blue-row {
background-color: #2244cc44;
}
.tile.red {
background-color: #cc333344;
}
.tile.green {
background-color: #33cc3344;
}
.tile.blue {
background-color: #2244cc44;
}
Dragging Between Grids Copy Link
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 HTMLElement and adding the rows should be handled by the onDragStop callback. If you wish the grid to manage the Drag and Drop across grids and also handle where the record should be dropped, take a look at Row Dragging - Grid to Grid
In the example below, note the following:
Rows can be dragged from one grid to the other grid. When the row is received, it is not removed from the first grid. This is the choice of the example. The example could equally have removed from the other grid.
If the row is already present in the grid, it will not be added twice, and a message is logged in the developer console. This happens because the grid will not allow duplicated IDs.
Rows can be removed from both grids by dragging the row to the 'Trash' drop zone.
New rows can be created by clicking on the red, green and blue buttons.
'use client';
import React, {
StrictMode,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { createRoot } from "react-dom/client";
import type {
ColDef,
GetRowIdParams,
GridApi,
GridReadyEvent,
RowDropZoneParams,
} from "ag-grid-community";
import {
ClientSideRowModelApiModule,
ClientSideRowModelModule,
RowApiModule,
RowDragModule,
RowStyleModule,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import { AgGridProvider, AgGridReact } from "ag-grid-react";
import "./styles.css";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
const modules = [
ClientSideRowModelApiModule,
RowApiModule,
TextFilterModule,
RowDragModule,
RowStyleModule,
ClientSideRowModelModule,
];
const columns: ColDef[] = [
{ field: "id", rowDrag: true },
{ field: "color" },
{ field: "value1" },
{ field: "value2" },
];
const rowClassRules = {
"red-row": 'data.color == "Red"',
"green-row": 'data.color == "Green"',
"blue-row": 'data.color == "Blue"',
};
const defaultColDef: ColDef = {
flex: 1,
minWidth: 100,
filter: true,
};
const GridExample = () => {
const [leftApi, setLeftApi] = useState<GridApi | null>(null);
const [rightApi, setRightApi] = useState<GridApi | null>(null);
const [leftRowData, setLeftRowData] = useState<any[]>([]);
const [rightRowData] = useState<any[]>([]);
const eLeftGrid = useRef(null);
const eRightGrid = useRef(null);
const eBin = useRef<HTMLElement>(null);
const eBinIcon = useRef<HTMLElement>(null);
let rowIdSequence = 100;
const createDataItem = useCallback(
(color: string) => {
const obj = {
id: rowIdSequence++,
color: color,
value1: Math.floor(window.agRandom() * 100),
value2: Math.floor(window.agRandom() * 100),
};
return obj;
},
[rowIdSequence],
);
useEffect(() => {
const createLeftRowData = () =>
["Red", "Green", "Blue"].map((color) => createDataItem(color));
setLeftRowData(createLeftRowData());
}, [createDataItem]);
const getRowId = (params: GetRowIdParams) => String(params.data.id);
const addRecordToGrid = (side: string, data: any) => {
// if data missing or data has no it, do nothing
if (!data || data.id == null) {
return;
}
const api = side === "left" ? leftApi : rightApi;
// do nothing if row is already in the grid, otherwise we would have duplicates
const rowAlreadyInGrid = !!api!.getRowNode(data.id);
let transaction;
if (rowAlreadyInGrid) {
console.log("not adding row to avoid duplicates in the grid");
return;
}
transaction = {
add: [data],
};
api!.applyTransaction(transaction);
};
const onFactoryButtonClick = (e: any) => {
const button = e.currentTarget,
buttonColor = button.getAttribute("data-color"),
side = button.getAttribute("data-side"),
data = createDataItem(buttonColor);
addRecordToGrid(side, data);
};
const binDrop = (data: any) => {
// if data missing or data has no id, do nothing
if (!data || data.id == null) {
return;
}
const transaction = {
remove: [data],
};
[leftApi, rightApi].forEach((api) => {
const rowsInGrid = !!api!.getRowNode(data.id);
if (rowsInGrid) {
api!.applyTransaction(transaction);
}
});
};
const addBinZone = (api: GridApi) => {
const dropZone: RowDropZoneParams = {
getContainer: () => eBinIcon.current!,
onDragEnter: () => {
eBin.current!.style.color = "blue";
eBinIcon.current!.style.transform = "scale(1.5)";
},
onDragLeave: () => {
eBin.current!.style.removeProperty("color");
eBinIcon.current!.style.transform = "scale(1)";
},
onDragStop: (params) => {
binDrop(params.node.data);
eBin.current!.style.removeProperty("color");
eBinIcon.current!.style.transform = "scale(1)";
},
};
api.addRowDropZone(dropZone);
};
const addGridDropZone = (side: string, api: GridApi) => {
const dropSide = side === "Left" ? "Right" : "Left";
const dropZone: RowDropZoneParams = {
getContainer: () =>
dropSide === "Right" ? eRightGrid.current! : eLeftGrid.current!,
onDragStop: (dragParams) =>
addRecordToGrid(dropSide.toLowerCase(), dragParams.node.data),
};
api.addRowDropZone(dropZone);
};
useEffect(() => {
if (rightApi && leftApi) {
addBinZone(rightApi);
addBinZone(leftApi);
addGridDropZone("Right", rightApi);
addGridDropZone("Left", leftApi);
}
});
const onGridReady = (side: string, params: GridReadyEvent) => {
if (side === "Left") {
setLeftApi(params.api);
} else {
setRightApi(params.api);
}
};
const getAddRecordButton = (side: string, color: string) => (
<button
key={`btn_${side}_${color}`}
className={`factory factory-${color.toLowerCase()}`}
data-color={color}
data-side={side.toLowerCase()}
onClick={onFactoryButtonClick}
>
<i className="far fa-plus-square"></i>
{`Add ${color}`}
</button>
);
const getInnerGridCol = (side: string) => (
<div className="inner-col">
<div className="toolbar">
{["Red", "Green", "Blue"].map((color) =>
getAddRecordButton(side, color),
)}
</div>
<div
style={{ height: "100%" }}
className="inner-col"
ref={side === "Left" ? eLeftGrid : eRightGrid}
>
<AgGridReact
defaultColDef={defaultColDef}
getRowId={getRowId}
rowClassRules={rowClassRules}
rowDragManaged={true}
suppressMoveWhenRowDragging={true}
rowData={side === "Left" ? leftRowData : rightRowData}
columnDefs={[...columns]}
onGridReady={(params: GridReadyEvent) => onGridReady(side, params)}
/>
</div>
</div>
);
return (
<AgGridProvider modules={modules}>
<div className="example-wrapper">
{getInnerGridCol("Left")}
<div className="inner-col vertical-toolbar">
<span className="bin" ref={eBin}>
<i className="far fa-trash-alt fa-3x" ref={eBinIcon}></i>
</span>
</div>
{getInnerGridCol("Right")}
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
.example-wrapper {
display: flex;
height: 100%;
}
.inner-col {
height: 100%;
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-width: 0;
}
.inner-col.vertical-toolbar {
display: flex;
flex: none;
width: 100px;
align-items: center;
justify-content: center;
}
.toolbar {
white-space: nowrap;
}
.vertical-toolbar > span {
padding: 10px;
margin: 10px;
cursor: default;
user-select: none;
-ms-user-select: none;
-moz-user-select: none;
-webkit-user-modify: none;
}
button.factory {
height: 25px;
border-radius: 5px;
border: none;
color: white;
outline: none;
cursor: pointer;
}
button i {
margin-right: 10px;
}
.bin i {
transform: scale(1);
transition: transform 500ms;
}
.factory-red {
background-color: #cc333344;
}
.factory-green {
background-color: #33cc3344;
}
.factory-blue {
background-color: #2244cc44;
}
.red-row {
background-color: #cc333344;
}
.green-row {
background-color: #33cc3344;
}
.blue-row {
background-color: #2244cc44;
}
.example-wrapper {
display: flex;
height: 100%;
}
.inner-col {
height: 100%;
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-width: 0;
}
.inner-col.vertical-toolbar {
display: flex;
flex: none;
width: 100px;
align-items: center;
justify-content: center;
}
.toolbar {
white-space: nowrap;
}
.vertical-toolbar > span {
padding: 10px;
margin: 10px;
cursor: default;
user-select: none;
-ms-user-select: none;
-moz-user-select: none;
-webkit-user-modify: none;
}
button.factory {
height: 25px;
border-radius: 5px;
border: none;
color: white;
outline: none;
cursor: pointer;
margin-right: 5px;
}
button i {
margin-right: 10px;
}
.bin i {
transform: scale(1);
transition: transform 500ms;
}
.factory-red {
background-color: #cc333344;
}
.factory-green {
background-color: #33cc3344;
}
.factory-blue {
background-color: #2244cc44;
}
.red-row {
background-color: #cc333344 !important;
}
.green-row {
background-color: #33cc3344 !important;
}
.blue-row {
background-color: #2244cc44 !important;
}