Row Drag Between Grids is concerned with seamless integration among different grids, allowing records to be dragged from one grid and dropped at a specific index on another grid.
Adds a drop zone outside of the grid where rows can be dropped. |
Removes an external drop zone added by addRowDropZone. |
Returns the RowDropZoneParams to be used by another grid's addRowDropZone method. |
Adding a Grid as Target Copy Link
To allow adding a grid as DropZone, the getRowDropZoneParams API method should be used in the target grid and the addRowDropZone in the source grid.
const dropZoneParams = targetGridApi.getRowDropZoneParams({
onDragStop: function() {
alert('Record Dropped!');
}
});
if (dropZoneParams) {
sourceGridApi.addRowDropZone(dropZoneParams);
// when the DropZone above is no longer needed
sourceGridApi.removeRowDropZone(dropZoneParams);
}In the example below, note the following:
When you drag from one grid to another, a line will appear indicating where the row will be placed.
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.
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.
import { Component, ViewChild } from "@angular/core";
import { AgGridAngular } from "ag-grid-angular";
import type {
ColDef,
GetRowIdParams,
GridApi,
GridReadyEvent,
RowDropZoneParams,
} from "ag-grid-community";
import {
ClientSideRowModelApiModule,
ClientSideRowModelModule,
ModuleRegistry,
RowApiModule,
RowDragModule,
RowStyleModule,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import "./styles.css";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
RowDragModule,
ClientSideRowModelApiModule,
RowApiModule,
TextFilterModule,
RowStyleModule,
ClientSideRowModelModule,
]);
@Component({
standalone: true,
imports: [AgGridAngular],
selector: "my-app",
template: `
<div class="example-wrapper">
<div class="inner-col">
<div class="toolbar">
<button
class="factory factory-red"
data-color="Red"
data-side="left"
(click)="onFactoryButtonClick($event)"
>
<i class="far fa-plus-square"></i>Add Red
</button>
<button
class="factory factory-green"
data-color="Green"
data-side="left"
(click)="onFactoryButtonClick($event)"
>
<i class="far fa-plus-square"></i>Add Green
</button>
<button
class="factory factory-blue"
data-color="Blue"
data-side="left"
(click)="onFactoryButtonClick($event)"
>
<i class="far fa-plus-square"></i>Add Blue
</button>
</div>
<div style="height: 100%;" class="inner-col" #eLeftGrid>
<ag-grid-angular
style="height: 100%;"
[defaultColDef]="defaultColDef"
[getRowId]="getRowId"
[rowClassRules]="rowClassRules"
[rowDragManaged]="true"
[suppressMoveWhenRowDragging]="true"
[rowData]="leftRowData"
[columnDefs]="columns"
(gridReady)="onGridReady($event, 'Left')"
/>
</div>
</div>
<div class="inner-col vertical-toolbar">
<span class="bin" #eBin>
<i class="far fa-trash-alt fa-3x" #eBinIcon></i>
</span>
</div>
<div class="inner-col">
<div class="toolbar">
<button
class="factory factory-red"
data-color="Red"
data-side="right"
(click)="onFactoryButtonClick($event)"
>
<i class="far fa-plus-square"></i>Add Red
</button>
<button
class="factory factory-green"
data-color="Green"
data-side="right"
(click)="onFactoryButtonClick($event)"
>
<i class="far fa-plus-square"></i>Add Green
</button>
<button
class="factory factory-blue"
data-color="Blue"
data-side="right"
(click)="onFactoryButtonClick($event)"
>
<i class="far fa-plus-square"></i>Add Blue
</button>
</div>
<div style="height: 100%;" class="inner-col" #eRightGrid>
<ag-grid-angular
style="height: 100%;"
[defaultColDef]="defaultColDef"
[getRowId]="getRowId"
[rowClassRules]="rowClassRules"
[rowDragManaged]="true"
[suppressMoveWhenRowDragging]="true"
[rowData]="rightRowData"
[columnDefs]="columns"
(gridReady)="onGridReady($event, 'Right')"
/>
</div>
</div>
</div>
`,
})
export class AppComponent {
leftRowData: any[] = [];
rightRowData: any[] = [];
leftApi!: GridApi;
rightApi!: GridApi;
rowClassRules = {
"red-row": 'data.color == "Red"',
"green-row": 'data.color == "Green"',
"blue-row": 'data.color == "Blue"',
};
defaultColDef: ColDef = {
flex: 1,
minWidth: 100,
filter: true,
};
columns: ColDef[] = [
{ field: "id", rowDrag: true },
{ field: "color" },
{ field: "value1" },
{ field: "value2" },
];
@ViewChild("eLeftGrid") eLeftGrid: any;
@ViewChild("eRightGrid") eRightGrid: any;
@ViewChild("eBin") eBin: any;
@ViewChild("eBinIcon") eBinIcon: any;
constructor() {
this.leftRowData = createRowBlock(2);
this.rightRowData = createRowBlock(2);
}
getRowId = (params: GetRowIdParams) => {
return String(params.data.id);
};
onGridReady(params: GridReadyEvent, side: string) {
const api = params.api;
if (side === "Left") {
this.leftApi = api;
} else {
this.rightApi = api;
}
if (this.leftApi && this.rightApi) {
this.addBinZone(this.leftApi);
this.addBinZone(this.rightApi);
this.addGridDropZone("Left", this.leftApi);
this.addGridDropZone("Right", this.rightApi);
}
}
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" ? this.leftApi : this.rightApi;
// do nothing if row is already in the grid, otherwise we would have duplicates
const rowAlreadyInGrid = !!api.getRowNode(data.id);
if (rowAlreadyInGrid) {
console.log("not adding row to avoid duplicates in the grid");
return;
}
const transaction = {
add: [data],
};
api.applyTransaction(transaction);
}
onFactoryButtonClick(e: any) {
const button = e.currentTarget,
buttonColor = button.getAttribute("data-color"),
side = button.getAttribute("data-side"),
data = createDataItem(buttonColor);
this.addRecordToGrid(side, data);
}
binDrop(data: any) {
// if data missing or data has no id, do nothing
if (!data || data.id == null) {
return;
}
const transaction = {
remove: [data],
};
[this.leftApi, this.rightApi].forEach((api) => {
const rowsInGrid = !!api.getRowNode(data.id);
if (rowsInGrid) {
api.applyTransaction(transaction);
}
});
}
addBinZone(api: GridApi) {
const dropZone: RowDropZoneParams = {
getContainer: () => this.eBinIcon.nativeElement,
onDragEnter: () => {
this.eBin.nativeElement.style.color = "blue";
this.eBinIcon.nativeElement.style.transform = "scale(1.5)";
},
onDragLeave: () => {
this.eBin.nativeElement.style = "";
this.eBinIcon.nativeElement.style.transform = "scale(1)";
},
onDragStop: (params) => {
this.binDrop(params.node.data);
this.eBin.nativeElement.style = "";
this.eBinIcon.nativeElement.style.transform = "scale(1)";
},
};
api.addRowDropZone(dropZone);
}
addGridDropZone(side: string, api: GridApi) {
const dropApi = side === "Left" ? this.rightApi : this.leftApi;
const dropZone = dropApi.getRowDropZoneParams();
api.addRowDropZone(dropZone!);
}
}
let rowIdSequence = 100;
function createDataItem(color: string) {
const obj = {
id: rowIdSequence++,
color: color,
value1: Math.floor(window.agRandom() * 100),
value2: Math.floor(window.agRandom() * 100),
};
return obj;
}
const createRowBlock = (blocks: any) =>
Array.apply(null, Array(blocks || 1))
.map(() => ["Red", "Green", "Blue"].map((color) => createDataItem(color)))
.reduce((prev, curr) => prev.concat(curr), []);
.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: 2px;
}
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;
}
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
if (new URLSearchParams(window.location.search).get('prod') !== 'false') {
enableProdMode();
}
const app = bootstrapApplication(AppComponent, {
providers: [provideHttpClient()],
});
Dragging Multiple Records Between Grids Copy Link
It is possible to drag multiple records at once from one grid to another.
In the example below, note the following:
This example enables Multi-Row Dragging between grids using
rowDragMultiRow.When
Remove Source Rowsis selected, the rows will be removed from the Athletes grid once they are dropped onto the Selected Athletes grid.If
Only Deselect Source Rowsis selected, all selected rows that were copied will be deselected but will not be removed.
Note: If some rows are selected and a row that isn't selected is copied, the selected rows will remain selected.If
Noneis selected, the rows will be copied from one grid to another and the source grid will stay as is.
import { HttpClient } from "@angular/common/http";
import { ChangeDetectionStrategy, Component, ViewChild } from "@angular/core";
import type { ICellRendererAngularComp } from "ag-grid-angular";
import { AgGridAngular } from "ag-grid-angular";
import type {
ColDef,
GetRowIdParams,
GridApi,
GridReadyEvent,
ICellRendererParams,
RowSelectionOptions,
} from "ag-grid-community";
import {
ClientSideRowModelApiModule,
ClientSideRowModelModule,
ModuleRegistry,
RowDragModule,
RowSelectionModule,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import "./styles.css";
// Enable extended validations only for development
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
RowDragModule,
ClientSideRowModelApiModule,
TextFilterModule,
RowSelectionModule,
ClientSideRowModelModule,
]);
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: ` <i
class="far fa-trash-alt"
style="cursor: pointer"
(click)="applyTransaction()"
></i>`,
})
export class SportRenderer implements ICellRendererAngularComp {
private params!: ICellRendererParams;
agInit(params: ICellRendererParams): void {
this.params = params;
}
applyTransaction() {
this.params.api.applyTransaction({ remove: [this.params.node.data] });
}
refresh() {
return false;
}
}
@Component({
standalone: true,
imports: [AgGridAngular],
selector: "my-app",
template: /*html */ ` <div class="top-container">
<div class="example-toolbar panel panel-default">
<div class="panel-body">
<input type="radio" id="move" name="radio" checked #eMoveRadio />
<label for="move">Remove Source Rows</label>
<input type="radio" id="deselect" name="radio" #eDeselectRadio />
<label for="deselect">Only Deselect Source Rows</label>
<input type="radio" id="none" name="radio" />
<label for="none">None</label>
<span class="input-group-button">
<button
type="button"
class="btn btn-default reset"
style="margin-left: 5px;"
(click)="reset()"
>
<i class="fas fa-redo" style="margin-right: 5px;"></i>Reset
</button>
</span>
</div>
</div>
<div class="grid-wrapper">
<div class="panel panel-primary" style="margin-right: 10px;">
<div class="panel-heading">Athletes</div>
<div class="panel-body">
<div id="eLeftGrid">
<ag-grid-angular
style="height: 100%;"
[defaultColDef]="defaultColDef"
[rowSelection]="rowSelection"
[rowDragMultiRow]="true"
[getRowId]="getRowId"
[rowDragManaged]="true"
[suppressMoveWhenRowDragging]="true"
[rowData]="leftRowData"
[columnDefs]="leftColumns"
(gridReady)="onGridReady($event, 0)"
/>
</div>
</div>
</div>
<div class="panel panel-primary" style="margin-left: 10px;">
<div class="panel-heading">Selected Athletes</div>
<div class="panel-body">
<div id="eRightGrid">
<ag-grid-angular
style="height: 100%;"
[defaultColDef]="defaultColDef"
[getRowId]="getRowId"
[rowDragManaged]="true"
[rowData]="rightRowData"
[columnDefs]="rightColumns"
(gridReady)="onGridReady($event, 1)"
/>
</div>
</div>
</div>
</div>
</div>`,
})
export class AppComponent {
rawData: any[] = [];
leftRowData: any[] = [];
rightRowData: any[] = [];
leftApi!: GridApi;
rightApi!: GridApi;
defaultColDef: ColDef = {
flex: 1,
minWidth: 100,
filter: true,
};
rowSelection: RowSelectionOptions = {
mode: "multiRow",
};
leftColumns: ColDef[] = [
{
rowDrag: true,
maxWidth: 50,
suppressHeaderMenuButton: true,
suppressHeaderFilterButton: true,
rowDragText: (params, dragItemCount) => {
if (dragItemCount > 1) {
return dragItemCount + " athletes";
}
return params.rowNode!.data.athlete;
},
},
{ field: "athlete" },
{ field: "sport" },
];
rightColumns: ColDef[] = [
{
rowDrag: true,
maxWidth: 50,
suppressHeaderMenuButton: true,
suppressHeaderFilterButton: true,
rowDragText: (params, dragItemCount) => {
if (dragItemCount > 1) {
return dragItemCount + " athletes";
}
return params.rowNode!.data.athlete;
},
},
{ field: "athlete" },
{ field: "sport" },
{
suppressHeaderMenuButton: true,
suppressHeaderFilterButton: true,
maxWidth: 50,
cellRenderer: SportRenderer,
},
];
@ViewChild("eLeftGrid") eLeftGrid: any;
@ViewChild("eRightGrid") eRightGrid: any;
@ViewChild("eMoveRadio") eMoveRadio: any;
@ViewChild("eDeselectRadio") eDeselectRadio: any;
constructor(private http: HttpClient) {
this.http
.get("https://www.ag-grid.com/example-assets/olympic-winners.json")
.subscribe((data) => {
const athletes: any[] = [];
let i = 0;
const dataArray = data as any[];
while (athletes.length < 20 && i < dataArray.length) {
var pos = i++;
if (athletes.some((rec) => rec.athlete === dataArray[pos].athlete)) {
continue;
}
athletes.push(dataArray[pos]);
}
this.rawData = athletes;
this.loadGrids();
});
}
loadGrids = () => {
this.leftRowData = [...this.rawData];
this.rightRowData = [];
};
reset = () => {
this.eMoveRadio.nativeElement.checked = true;
this.loadGrids();
};
getRowId = (params: GetRowIdParams) => params.data.athlete;
onGridReady(params: GridReadyEvent, side: number) {
if (side === 0) {
this.leftApi = params.api;
}
if (side === 1) {
this.rightApi = params.api;
this.addGridDropZone();
}
}
addGridDropZone() {
const dropZoneParams = this.rightApi.getRowDropZoneParams({
onDragStop: (params) => {
const deselectCheck = this.eDeselectRadio.nativeElement.checked;
const moveCheck = this.eMoveRadio.nativeElement.checked;
const nodes = params.nodes;
if (moveCheck) {
this.leftApi.applyTransaction({
remove: nodes.map(function (node) {
return node.data;
}),
});
} else if (deselectCheck) {
this.leftApi.setNodesSelected({ nodes, newValue: false });
}
},
});
this.leftApi.addRowDropZone(dropZoneParams!);
}
}
.top-container {
height: 100%;
display: flex;
flex-direction: column;
}
.example-toolbar label {
margin: 0 15px 0 0;
}
.example-toolbar input[type='radio'] {
margin: 0 0.25rem 0 0;
}
.grid-wrapper {
display: flex;
flex: 1 1 auto;
margin-top: 5px;
}
.grid-wrapper .panel {
flex: 1 1 50%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.grid-wrapper .panel-body {
flex: 1 1 auto;
overflow: hidden;
padding: 0;
display: flex;
}
.grid-wrapper .panel-body > div {
width: 100%;
}
.top-container {
height: 100%;
display: flex;
flex-direction: column;
}
.example-toolbar label {
margin: 0 15px 0 0;
}
.example-toolbar input[type='radio'] {
margin: 0 0.25rem 0 0;
}
.grid-wrapper {
display: flex;
flex: 1 1 auto;
margin-top: 5px;
}
.grid-wrapper .panel {
flex: 1 1 50%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.grid-wrapper .panel-body {
flex: 1 1 auto;
overflow: hidden;
padding: 0;
display: flex;
}
.grid-wrapper .panel-body > div {
width: 100%;
}
import '@angular/compiler';
import { provideHttpClient } from '@angular/common/http';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
if (new URLSearchParams(window.location.search).get('prod') !== 'false') {
enableProdMode();
}
const app = bootstrapApplication(AppComponent, {
providers: [provideHttpClient()],
});