Rows can be rearranged interactively when using Tree Data by dragging with the mouse.
Enabling Row Dragging Copy Link
To enable row dragging, set rowDrag: true on the group column (usually via autoGroupColumnDef).
const treeData = true;
const autoGroupColumnDef = useMemo(() => {
return {
field: 'name',
rowDrag: true // Enable row dragging on the group column
};
}, []);
<AgGridReact
treeData={treeData}
autoGroupColumnDef={autoGroupColumnDef}
/>See the Row Dragging documentation for more information about row dragging options, APIs, and advanced usage.
There are two approaches to enable Row Dragging:
- Managed Row Dragging: The grid handles row dragging automatically.
- Unmanaged Row Dragging: Customized application-specific logic for row dragging.
Enabling Managed Row Dragging Copy Link
This is the simplest way to enable row dragging with Tree Data. The grid will automatically handle the dragging of rows and updating the data structure. It supports reordering, moving parents and children, and converting a leaf node into a group. Moving a parent to be a child of itself is not allowed, as this would create a cycle. The grid will prevent this automatically.
To enable managed row dragging, set the following options:
rowDragManaged: true— Enables managed row dragging, so the grid handles row movement automatically.autoGroupColumnDef.rowDrag: true— Enables the drag handle in the group column.suppressMoveWhenRowDragging: true— Prevents the grid from moving rows while dragging, showing a highlight over the row instead.
It is recommended to enable suppressMoveWhenRowDragging when using managed row dragging with Tree Data. Without this option, moving subtrees can cause the grid to jump or scroll unexpectedly as rows are repositioned during the drag. Enabling it provides a smoother and more predictable user experience by only highlighting the drop target without moving rows until the drop is complete.
"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 {
AutoGroupColumnDef,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GetRowIdFunc,
GridApi,
GridOptions,
ModuleRegistry,
RowDragModule,
enableDevValidations,
} from "ag-grid-community";
import { TreeDataModule } from "ag-grid-enterprise";
import { Task, getData } from "./data";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [ClientSideRowModelModule, TreeDataModule, RowDragModule];
const eGridDiv = document.getElementById("myGrid");
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<Task[]>(getData());
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "assignee" },
]);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
headerName: "Task",
field: "title",
rowDrag: true,
flex: 2,
minWidth: 200,
};
}, []);
const getRowId = useCallback((params) => params.data.id, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact<Task>
rowData={rowData}
columnDefs={columnDefs}
autoGroupColumnDef={autoGroupColumnDef}
getRowId={getRowId}
treeData={true}
treeDataParentIdField={"parentId"}
groupDefaultExpanded={-1}
rowDragManaged={true}
suppressMoveWhenRowDragging={true}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
Other relevant options used in the example above include:
getRowId— Provides a unique ID for each row, required for row movement.treeData: true— Enables tree data mode, allowing hierarchical data structures.treeDataParentIdField: 'parentId'— Specifies the field that defines parent-child relationships.groupDefaultExpanded: -1— Expands all groups by default.
const treeData = true;
const getRowId = useCallback(params => params.data.id, []);
const treeDataParentIdField = 'parentId';
const rowDragManaged = true;
const groupDefaultExpanded = -1;
const suppressMoveWhenRowDragging = true;
const autoGroupColumnDef = useMemo(() => {
return {
field: 'name',
rowDrag: true
};
}, []);
<AgGridReact
treeData={treeData}
getRowId={getRowId}
treeDataParentIdField={treeDataParentIdField}
rowDragManaged={rowDragManaged}
groupDefaultExpanded={groupDefaultExpanded}
suppressMoveWhenRowDragging={suppressMoveWhenRowDragging}
autoGroupColumnDef={autoGroupColumnDef}
/> Managed Row Dragging with getDataPath Copy Link
This next examples shows how to use the getDataPath callback to define the hierarchical structure of the data.
This example uses filler nodes (where some intermediate path segments do not exist as explicit nodes in the data). Empty filler nodes cannot exist in the grid; if all their children are moved out, the filler node will be deleted and disappear. It is instead recommended to provide a full grid without filler nodes to avoid this. See the Providing Data Paths for details about filler nodes and getDataPath.
"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 {
AutoGroupColumnDef,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GetDataPath,
GetRowIdFunc,
GridApi,
GridOptions,
ModuleRegistry,
RowDragModule,
enableDevValidations,
} from "ag-grid-community";
import { TreeDataModule } from "ag-grid-enterprise";
import { Task, getData } from "./data";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [ClientSideRowModelModule, TreeDataModule, RowDragModule];
const eGridDiv = document.getElementById("myGrid");
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<Task[]>(getData());
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "assignee" },
]);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
headerName: "Task",
rowDrag: true,
flex: 2,
minWidth: 200,
};
}, []);
const getRowId = useCallback((params) => params.data.id, []);
const getDataPath = useCallback((data) => data.path, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact<Task>
rowData={rowData}
columnDefs={columnDefs}
autoGroupColumnDef={autoGroupColumnDef}
getRowId={getRowId}
treeData={true}
getDataPath={getDataPath}
groupDefaultExpanded={-1}
rowDragManaged={true}
suppressMoveWhenRowDragging={true}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
Multi-Row Dragging Copy Link
Managed row dragging supports multi-row dragging, allowing users to select multiple rows and drag them together, including rows in different levels.
To enable this, set the grid options rowDragMultiRow = true together with rowSelection.mode = 'multiRow'.
For this example note the following:
- When you select multiple items and drag one of them, all items in the selection will be dragged.
- When you drag an item that is not selected while other items are selected, only the unselected item will be dragged.
"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 {
AutoGroupColumnDef,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GetRowIdFunc,
GridApi,
GridOptions,
ModuleRegistry,
RowDragModule,
RowSelectionModule,
RowSelectionOptions,
enableDevValidations,
} from "ag-grid-community";
import { TreeDataModule } from "ag-grid-enterprise";
import { Task, getData } from "./data";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ClientSideRowModelModule,
TreeDataModule,
RowDragModule,
RowSelectionModule,
];
const eGridDiv = document.getElementById("myGrid");
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<Task[]>(getData());
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "assignee" },
]);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
headerName: "Task",
field: "title",
rowDrag: true,
flex: 2,
minWidth: 200,
};
}, []);
const getRowId = useCallback((params) => params.data.id, []);
const rowSelection = useMemo<
RowSelectionOptions | "single" | "multiple"
>(() => {
return {
mode: "multiRow",
};
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact<Task>
rowData={rowData}
columnDefs={columnDefs}
autoGroupColumnDef={autoGroupColumnDef}
getRowId={getRowId}
treeData={true}
treeDataChildrenField={"children"}
groupDefaultExpanded={-1}
rowDragManaged={true}
rowDragMultiRow={true}
rowSelection={rowSelection}
suppressMoveWhenRowDragging={true}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
Row Drag Insert Delay Copy Link
When using Tree Data with Managed Row Dragging, the rowDragInsertDelay grid option sets a delay (in milliseconds) before a dragged row is inserted into a new parent node. The default value is 500 milliseconds. This delay helps prevent accidental moves when hovering over potential drop targets. If the target is a collapsed parent or a leaf node, the grid will expand the parent or convert the leaf into a parent after this delay, allowing the dragged row to be inserted as a child.
Preventing Dropping on Certain Rows Copy Link
The isRowValidDropPosition callback allows you to control whether a row drop is allowed during managed or unmanaged row dragging, and optionally override the rows, parent or position for the drop. This is useful for restricting where rows can be dropped or customizing drop behaviour. Returning an object allows instead to filter the rows to drop, or change the parent or the position of the drop.
This affects also the icon and label shown when dragging a row for both managed and unmanaged row dragging.
Called by drag and drop when rows are dragged over another row to conditionally prevent dropping the dragged row on the hovered row.
The user can cancel the drop by returning false or customize the operation by returning a IsRowValidDropPositionResult. |
In the example below, note that:
- A file cannot be converted to a folder, dropping a file or a folder into a file is blocked.
- The
READONLYfolder cannot change parent, and drag and drop into or from it is not allowed.
"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 {
AutoGroupColumnDef,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GetRowIdFunc,
GridApi,
GridOptions,
IRowNode,
IsRowValidDropPositionCallback,
ModuleRegistry,
RowDragModule,
ValueFormatterParams,
enableDevValidations,
} from "ag-grid-community";
import { TreeDataModule } from "ag-grid-enterprise";
import { IFile, getData } from "./data";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [ClientSideRowModelModule, TreeDataModule, RowDragModule];
/** Returns true if the row is a readonly folder, false if it is a file or a normal folder */
function isReadonlyFolder(row: IRowNode<IFile> | null) {
return !!row && row.data?.type === "readonly-folder";
}
/** Returns true if the row is a file or folder inside a readonly folder */
const isInsideReadonlyFolder: (row: IRowNode<IFile> | null) => boolean = (
row: IRowNode<IFile> | null,
) => {
if (!row || !row.parent) {
return false; // Root level
}
if (isReadonlyFolder(row.parent)) {
return true;
}
return isInsideReadonlyFolder(row.parent);
};
const eGridDiv = document.getElementById("myGrid");
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<IFile[]>(getData());
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{
field: "type",
headerName: "Type",
width: 90,
},
{
field: "dateModified",
headerName: "Modified",
width: 130,
},
{
field: "size",
aggFunc: "sum",
width: 140,
valueFormatter: (params: ValueFormatterParams<IFile, number>) =>
params.value ? params.value.toFixed(1) + " MB" : "",
},
]);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
headerName: "Task",
field: "name",
rowDrag: true,
flex: 2,
minWidth: 200,
};
}, []);
const getRowId = useCallback((params) => params.data.id, []);
const isRowValidDropPosition = useCallback((params) => {
let { newParent, rows, moved } = params;
if (!moved) {
return { allowed: false };
}
if (isReadonlyFolder(newParent) || isInsideReadonlyFolder(newParent)) {
return { allowed: false }; // Prevent dropping into a readonly folder
}
// Filter out anything that is a readonly folder or inside a readonly folder
rows = rows.filter(
(row) => !isReadonlyFolder(row) && !isInsideReadonlyFolder(row),
);
if (newParent && newParent.data && newParent.data.type !== "folder") {
// Block changing parents on anything that is not of type 'folder'
return { newParent: null, rows };
}
return { rows };
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact<IFile>
rowData={rowData}
columnDefs={columnDefs}
autoGroupColumnDef={autoGroupColumnDef}
getRowId={getRowId}
treeData={true}
treeDataChildrenField={"children"}
groupDefaultExpanded={-1}
rowDragManaged={true}
suppressMoveWhenRowDragging={true}
isRowValidDropPosition={isRowValidDropPosition}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
Persisting Row Order Copy Link
These three examples below show how to persist the row order from the grid on to the server after a row drag operation has been completed.
Example with Parent IDs:
"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 {
AutoGroupColumnDef,
ClientSideRowModelApiModule,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GetRowIdFunc,
GridApi,
GridOptions,
ModuleRegistry,
RowDragModule,
enableDevValidations,
} from "ag-grid-community";
import { TreeDataModule } from "ag-grid-enterprise";
import { Task, getData } from "./data";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ClientSideRowModelModule,
ClientSideRowModelApiModule,
TreeDataModule,
RowDragModule,
];
function extractRowData(api: GridApi<Task>) {
const extractedData: Task[] = [];
api.forEachLeafNode((node) => {
let data = node.data!;
const parentId = node.parent?.data?.id;
if (data.parentId !== parentId) {
// We create a new object only if the parentId has changed
data = { ...data, parentId };
}
extractedData.push(data);
});
return extractedData;
}
function showExtractedRowData(api: GridApi<Task>) {
const extractedRowData = extractRowData(api);
const json = JSON.stringify(extractedRowData, null, 2);
document.getElementById("extracted-data-content")!.textContent = json;
}
const eGridDiv = document.getElementById("myGrid");
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<Task[]>(getData());
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "assignee" },
]);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
headerName: "Task",
field: "title",
rowDrag: true,
flex: 2,
minWidth: 200,
};
}, []);
const getRowId = useCallback((params) => params.data.id, []);
const onRowDragEnd = useCallback((event) => {
showExtractedRowData(event.api);
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="container">
<div style={gridStyle}>
<AgGridReact<Task>
rowData={rowData}
columnDefs={columnDefs}
autoGroupColumnDef={autoGroupColumnDef}
getRowId={getRowId}
treeData={true}
treeDataParentIdField={"parentId"}
groupDefaultExpanded={-1}
rowDragManaged={true}
suppressMoveWhenRowDragging={true}
onRowDragEnd={onRowDragEnd}
/>
</div>
<div id="extracted-data-content-container">
<pre id="extracted-data-content">output</pre>
</div>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
Example with Children arrays:
"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 {
AutoGroupColumnDef,
ClientSideRowModelApiModule,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GetRowIdFunc,
GridApi,
GridOptions,
IRowNode,
ModuleRegistry,
RowApiModule,
RowDragModule,
enableDevValidations,
} from "ag-grid-community";
import { TreeDataModule } from "ag-grid-enterprise";
import { Task, getData } from "./data";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ClientSideRowModelModule,
ClientSideRowModelApiModule,
RowApiModule,
TreeDataModule,
RowDragModule,
];
function arrayEquals<T>(a: T[], b: T[]) {
return a === b || (a.length === b.length && a.every((v, i) => v === b[i]));
}
/** Recursively build the tree structure from a node */
const buildTree: (node: IRowNode<Task>) => Task = (node: IRowNode<Task>) => {
const data = node.data!;
const oldChildren = data.children ?? [];
const children = node.childrenAfterGroup?.map(buildTree) ?? [];
if (!arrayEquals(oldChildren, children)) {
// We create a new object only if the children have changed
return { ...data, children: children.length > 0 ? children : undefined };
}
return data; // unchanged
};
/** Extract children for each node in the tree */
function extractRowData(rootNode: IRowNode<Task> | undefined) {
return rootNode?.childrenAfterGroup?.map(buildTree) ?? [];
}
function showExtractedRowData(rootNode: IRowNode<Task> | undefined) {
const extractedRowData = extractRowData(rootNode);
const json = JSON.stringify(extractedRowData, null, 2);
document.getElementById("extracted-data-content")!.textContent = json;
}
const eGridDiv = document.getElementById("myGrid");
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<Task[]>(getData());
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "assignee" },
]);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
headerName: "Task",
field: "title",
rowDrag: true,
flex: 2,
minWidth: 200,
};
}, []);
const getRowId = useCallback((params) => params.data.id, []);
const onRowDragEnd = useCallback((event) => {
showExtractedRowData(event.rowsDrop?.rootNode);
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="container">
<div style={gridStyle}>
<AgGridReact<Task>
rowData={rowData}
columnDefs={columnDefs}
autoGroupColumnDef={autoGroupColumnDef}
getRowId={getRowId}
treeData={true}
treeDataChildrenField={"children"}
groupDefaultExpanded={-1}
rowDragManaged={true}
suppressMoveWhenRowDragging={true}
onRowDragEnd={onRowDragEnd}
/>
</div>
<div id="extracted-data-content-container">
<pre id="extracted-data-content">output</pre>
</div>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
Example with Data Paths:
"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 {
AutoGroupColumnDef,
ClientSideRowModelApiModule,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GetDataPath,
GetRowIdFunc,
GridApi,
GridOptions,
IRowNode,
ModuleRegistry,
RowApiModule,
RowDragModule,
enableDevValidations,
} from "ag-grid-community";
import { TreeDataModule } from "ag-grid-enterprise";
import { Task, getData } from "./data";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ClientSideRowModelModule,
ClientSideRowModelApiModule,
RowApiModule,
TreeDataModule,
RowDragModule,
];
function arrayEquals<T>(a: T[], b: T[]) {
return a === b || (a.length === b.length && a.every((v, i) => v === b[i]));
}
// Rebuild the data array, updating the path for each node if changed
function extractRowData(api: GridApi<Task>) {
const extractedData: Task[] = [];
api.forEachLeafNode((node) => {
const data = node.data;
if (data) {
// Use getRoute() to rebuild the path
const path = node.getRoute() ?? [];
if (!arrayEquals(data.path, path)) {
// Create a new object only if the path has changed
extractedData.push({ ...data, path });
} else {
extractedData.push(data);
}
}
});
return extractedData;
}
function showExtractedRowData(api: GridApi<Task>) {
const extractedRowData = extractRowData(api);
const json = JSON.stringify(extractedRowData, null, 2);
document.getElementById("extracted-data-content")!.textContent = json;
}
const eGridDiv = document.getElementById("myGrid");
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<Task[]>(getData());
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "assignee" },
]);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
headerName: "Task",
rowDrag: true,
flex: 2,
minWidth: 200,
};
}, []);
const getRowId = useCallback((params) => params.data.id, []);
const getDataPath = useCallback((data) => data.path, []);
const onRowDragEnd = useCallback((event) => {
showExtractedRowData(event.api);
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="container">
<div style={gridStyle}>
<AgGridReact<Task>
rowData={rowData}
columnDefs={columnDefs}
autoGroupColumnDef={autoGroupColumnDef}
getRowId={getRowId}
treeData={true}
getDataPath={getDataPath}
groupDefaultExpanded={-1}
rowDragManaged={true}
suppressMoveWhenRowDragging={true}
onRowDragEnd={onRowDragEnd}
/>
</div>
<div id="extracted-data-content-container">
<pre id="extracted-data-content">output</pre>
</div>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
Unmanaged Row Dragging Copy Link
In order to have full control over row dragging, it is possible to provide a customized implementation of row dragging using Unmanaged Row Dragging. In this case, the application is responsible for maintaining the rowData state, handling the dragging events and updating the rowData based on the drag events fired by the grid.
Tree Data with getDataPath Copy Link
The example below shows Tree Data and row dragging with getDataPath where the following can be noted:
- The Auto-Group Column has row drag
truefor all rows. - The application moves the rows in the row data while the row drag is happening in the
onRowDragMoveevent handler. - While row dragging, the row move operation can be reverted by pressing ⎋ Escape key.
- Is possible to reorder a row only inside its current parent by holding the ⇧ Shift key and dragging it.
- The expanded/contracted state of a folder and all of its child folders is preserved when the folder is moved to a new parent.
"use client";
import React, { StrictMode, useCallback, useState } from "react";
import { createRoot } from "react-dom/client";
import {
ClientSideRowModelApiModule,
ClientSideRowModelModule,
RowDragModule,
enableDevValidations,
} from "ag-grid-community";
import type {
GridOptions,
RowDragEndEvent,
RowDragMoveEvent,
ValueFormatterParams,
} from "ag-grid-community";
import { TreeDataModule } from "ag-grid-enterprise";
import { AgGridProvider, AgGridReact } from "ag-grid-react";
import { getData } from "./data";
import FileCellRenderer from "./fileCellRenderer";
import { moveFiles } from "./fileUtils";
import type { IFile } from "./fileUtils";
import "./style.css";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ClientSideRowModelApiModule,
ClientSideRowModelModule,
RowDragModule,
TreeDataModule,
];
const STATIC_GRID_OPTIONS: GridOptions<IFile> = {
columnDefs: [
{ field: "dateModified" },
{
field: "size",
aggFunc: "sum",
valueFormatter: (params: ValueFormatterParams<IFile, number>) =>
params.value ? params.value.toFixed(1) + " MB" : "",
},
],
autoGroupColumnDef: {
rowDrag: true,
headerName: "Files",
minWidth: 300,
cellRendererParams: {
suppressCount: true,
innerRenderer: FileCellRenderer,
},
},
defaultColDef: { flex: 1 },
groupDefaultExpanded: -1,
treeData: true,
getDataPath: (data: IFile): string[] => data.filePath,
getRowId: (params): string => params.data.id,
};
const DragAndDropGrid = () => {
// The row data is stored in a state variable
const [rowData, setRowData] = useState<any[]>(getData);
// This will store a copy of rowData to perform cancel operation when user presses ESC while dragging
const [rowDataDragging, setRowDataDragging] = useState<IFile[] | null>(null);
/** Called when row dragging start */
const onRowDragEnter = useCallback(() => {
// Store the original row data to restore it the drag is cancelled in a custom property in the context
setRowDataDragging(rowData);
}, [rowData]);
/** Called both when dragging and dropping (drag end) */
const onRowDragMove = useCallback(
(event: RowDragMoveEvent<IFile> | RowDragEndEvent<IFile>) => {
let target = event.overNode?.data;
const source = event.node.data;
if (rowData && source && source !== target) {
const reorderOnly = event.event?.shiftKey;
const newRowData = moveFiles(rowData, source, target, reorderOnly);
setRowData(newRowData);
}
},
[rowData],
);
/** Called when row dragging end, and the operation need to be committed */
const onRowDragEnd = useCallback(
(event: RowDragEndEvent<IFile>) => {
event.api.clearFocusedCell();
setRowDataDragging(null);
onRowDragMove(event);
},
[onRowDragMove],
);
/** Called when row dragging is cancelled, for example, ESC key is pressed */
const onRowDragCancel = useCallback(() => {
if (rowDataDragging) {
// Restore the original row data before the drag started
setRowData(rowDataDragging);
setRowDataDragging(null);
}
}, [rowDataDragging]);
return (
<AgGridProvider modules={modules}>
<AgGridReact<IFile>
gridOptions={STATIC_GRID_OPTIONS}
rowData={rowData}
onRowDragEnter={onRowDragEnter}
onRowDragMove={onRowDragMove}
onRowDragEnd={onRowDragEnd}
onRowDragCancel={onRowDragCancel}
/>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<div className="myGrid">
<DragAndDropGrid />
</div>
</StrictMode>,
);
Tree Data with getDataPath, Highlighting the Drop Parent Row Copy Link
The example above works, however it is not intuitive as the user is given no visual hint what folder will be the destination folder. The example below continues with the example above by providing hints to the user while the drag is in progress. From the example the following can be observed:
- The example registers for
onRowDragMoveevents and works out which folder the mouse is over as the drag is happening. - While the row is dragging, the application highlights the folder that is currently selected as the destination folder (called
potentialParentin the example code). - The application does NOT rearrange the rows as the drag is happening. As with the previous example, it waits for the
onRowDragEndevent before updating the data. - The example uses Cell Class Rules to highlight the destination folder. The example adds a CSS class
hover-overto all the cells of the destination folder. - The example uses Refresh Cells to get the grid to execute the Cell Class Rules again over the destination folder when the destination folder changes.
("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 {
AutoGroupColumnDef,
CellClassParams,
CellStyleModule,
ClientSideRowModelApiModule,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GetDataPath,
GetRowIdFunc,
GridApi,
GridOptions,
ICellRendererParams,
IRowNode,
ModuleRegistry,
RefreshCellsParams,
RenderApiModule,
RowDragEndEvent,
RowDragLeaveEvent,
RowDragModule,
RowDragMoveEvent,
ValueFormatterParams,
enableDevValidations,
} from "ag-grid-community";
import { TreeDataModule } from "ag-grid-enterprise";
import { getData } from "./data";
import { IFile, getFileCssIcon, moveFiles } from "./fileUtils";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
RowDragModule,
ClientSideRowModelApiModule,
RenderApiModule,
CellStyleModule,
ClientSideRowModelModule,
TreeDataModule,
];
const valueFormatter = function (params: ValueFormatterParams<IFile, number>) {
return params.value ? params.value.toFixed(1) + " MB" : "";
};
const cellClassRules = {
"hover-over": (params: CellClassParams) => {
return params.node === potentialParent;
},
};
var potentialParent: any = null;
function setPotentialParentForNode(
api: GridApi<IFile>,
overNode: IRowNode<IFile> | undefined | null,
) {
let newPotentialParent: IRowNode<IFile> | null = null;
if (overNode) {
if (overNode.data?.type === "folder") {
// over a folder, we take the immediate row
newPotentialParent = overNode;
} else if (overNode.parent) {
// over a file, we take the parent row (which will be a folder)
newPotentialParent = overNode.parent;
}
}
const alreadySelected = potentialParent === newPotentialParent;
if (alreadySelected) {
return; // no change
}
// we refresh the previous selection (if it exists) to clear
// the highlighted and then the new selection.
const rowsToRefresh = [];
if (potentialParent) {
rowsToRefresh.push(potentialParent);
}
if (newPotentialParent) {
rowsToRefresh.push(newPotentialParent);
}
potentialParent = newPotentialParent;
refreshRows(api, rowsToRefresh);
}
function refreshRows(api: GridApi, rowsToRefresh: IRowNode<IFile>[]) {
const params: RefreshCellsParams<IFile> = {
// refresh these rows only.
rowNodes: rowsToRefresh,
// because the grid does change detection, the refresh
// will not happen because the underlying value has not
// changed. to get around this, we force the refresh,
// which skips change detection.
force: true,
};
api.refreshCells(params);
}
class FileCellRenderer {
private eGui!: any;
init(params: ICellRendererParams<IFile>) {
const eGui = document.createElement("div");
const eIcon = document.createElement("i");
eIcon.className = getFileCssIcon(params.data?.type, params.value);
const eFilename = document.createElement("span");
eFilename.className = "filename";
eFilename.innerText = params.value;
eGui.appendChild(eIcon);
eGui.appendChild(eFilename);
this.eGui = eGui;
}
getGui() {
return this.eGui;
}
}
const GridExample = () => {
const gridRef = useRef<AgGridReact<IFile>>(null);
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<IFile[]>(getData());
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{
field: "dateModified",
cellClassRules: cellClassRules,
},
{
field: "size",
aggFunc: "sum",
valueFormatter: valueFormatter,
cellClassRules: cellClassRules,
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
};
}, []);
const getDataPath = useCallback((data: IFile) => data.filePath, []);
const getRowId = useCallback(({ data }) => data.id, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
rowDrag: true,
headerName: "Files",
minWidth: 300,
cellRendererParams: {
suppressCount: true,
innerRenderer: FileCellRenderer,
},
cellClassRules: {
"hover-over": (params) => {
return params.node === potentialParent;
},
},
};
}, []);
const onRowDragMove = useCallback((event: RowDragMoveEvent) => {
setPotentialParentForNode(event.api, event.overNode);
}, []);
const onRowDragLeave = useCallback((event: RowDragLeaveEvent) => {
// clear node to highlight
setPotentialParentForNode(event.api, null);
}, []);
const onRowDragEnd = useCallback(
(event: RowDragEndEvent) => {
let target = event.overNode?.data;
if (!potentialParent && target) {
return; // no move
}
const source = event.node.data;
const rowData = event.api.getGridOption("rowData");
if (rowData && source && source !== target) {
const newRowData = moveFiles(rowData, source, target);
if (!newRowData) {
console.log("invalid move");
} else if (newRowData !== rowData) {
event.api.setGridOption("rowData", newRowData);
}
gridRef.current!.api.clearFocusedCell();
}
// clear node to highlight
setPotentialParentForNode(event.api, null);
},
[moveFiles],
);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact<IFile>
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
treeData={true}
groupDefaultExpanded={-1}
getDataPath={getDataPath}
getRowId={getRowId}
autoGroupColumnDef={autoGroupColumnDef}
onRowDragMove={onRowDragMove}
onRowDragLeave={onRowDragLeave}
onRowDragEnd={onRowDragEnd}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
Tree Data with Parent ID Copy Link
The following example shows how to implement unmanaged row dragging using the parentId approach, which is simpler and more direct than using getDataPath. The grid uses the treeDataParentIdField property, and utility functions are provided to move rows and update the tree structure. This approach is recommended for most use cases where your data is already structured with parent IDs.
This example also demonstrates how to provide custom drop indicators using the setRowDropPositionIndicator API.
"use client";
import React, { StrictMode, useCallback, useState } from "react";
import { createRoot } from "react-dom/client";
import {
ClientSideRowModelModule,
RowApiModule,
RowDragModule,
enableDevValidations,
} from "ag-grid-community";
import type {
GridOptions,
RowDragEndEvent,
RowDragEvent,
RowDragMoveEvent,
ValueFormatterParams,
} from "ag-grid-community";
import { TreeDataModule } from "ag-grid-enterprise";
import { AgGridProvider, AgGridReact } from "ag-grid-react";
import { getData } from "./data";
import type { IFile } from "./fileUtils";
import { getFileDropPosition, moveFiles } from "./fileUtils";
import "./style.css";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ClientSideRowModelModule,
RowApiModule,
RowDragModule,
TreeDataModule,
];
const STATIC_GRID_OPTIONS: GridOptions<IFile> = {
columnDefs: [
{
field: "type",
headerName: "Type",
width: 90,
},
{
field: "dateModified",
headerName: "Modified",
width: 130,
},
{
field: "size",
aggFunc: "sum",
width: 140,
valueFormatter: (params: ValueFormatterParams<IFile, number>) =>
params.value ? params.value.toFixed(1) + " MB" : "",
},
],
autoGroupColumnDef: {
rowDrag: true,
field: "name",
headerName: "Files",
minWidth: 400,
cellRendererParams: { suppressCount: true },
},
treeData: true,
getRowId: (params) => params.data.id,
treeDataParentIdField: "parentId",
rowData: getData(),
animateRows: true,
groupDefaultExpanded: -1,
};
const DragAndDropGrid = () => {
const [rowData, setRowData] = useState<IFile[]>(getData());
const gridRef = React.useRef<AgGridReact<IFile> | null>(null);
const onRowDragMove = useCallback(
(event: RowDragMoveEvent<IFile>) => {
const source = event.node.data;
const target = event.overNode?.data;
const reorderOnly = event.event?.shiftKey;
const position = getFileDropPosition(
rowData,
source,
target,
!!reorderOnly,
);
event.api.setRowDropPositionIndicator({
row: position?.target && event.api.getRowNode(position.target.id),
dropIndicatorPosition: position?.position || "none",
});
},
[rowData],
);
const onRowDragEnd = useCallback(
(event: RowDragEndEvent<IFile>) => {
const source = event.node.data;
const target = event.overNode?.data;
const reorderOnly = event.event?.shiftKey;
const position = getFileDropPosition(
rowData,
source,
target,
!!reorderOnly,
);
if (position) {
const newRowData = moveFiles(rowData, position);
setRowData(newRowData);
}
event.api.setRowDropPositionIndicator(null);
},
[rowData],
);
const onRowDragCancel = useCallback((event: RowDragEvent<IFile>) => {
event.api.setRowDropPositionIndicator(null);
}, []);
return (
<AgGridProvider modules={modules}>
<AgGridReact<IFile>
ref={gridRef}
gridOptions={STATIC_GRID_OPTIONS}
rowData={rowData}
onRowDragMove={onRowDragMove}
onRowDragEnd={onRowDragEnd}
onRowDragCancel={onRowDragCancel}
/>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<div className="myGrid">
<DragAndDropGrid />
</div>
</StrictMode>,
);
See Also Copy Link
- Aggregation for aggregating values in tree data
- Editing Groups for editing aggregated values with cascading updates to children