I'm building a React application that utilizes the ag-Grid library (https://www.ag-grid.com/react-data-grid/row-dragging-to-grid/) for displaying tabular data. I have two ag-Grid tables: one on the left and the other on the right. My goal is to allow users to drag and drop rows from the left grid to the right grid.
To achieve this, I have implemented the drag-and-drop functionality, and it works perfectly when there is data available in the left grid. However, I want to enhance the user experience by showing default categories ("Male" and "Female") in the right grid, even when there are no rows present in either grid.
Currently, I face two main challenges:
How can I display the "Male" and "Female" categories in the right grid by default, even when there is no data available in either grid?
When dragging rows from the left grid to the right grid, how can I ensure that the dropped rows are automatically placed under the appropriate default category (e.g., "Male" category for male athletes) in the right grid?
To summarize my requirements:
Show default "Male" and "Female" categories in the right grid, even when there is no data present in either grid.
Allow users to drag and drop rows from the left grid to the right grid.
Automatically place the dropped rows under the appropriate default category (e.g., "Male" category for male athletes) in the right grid.
This is the code I am using.
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import React, { useCallback, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { AgGridReact } from 'ag-grid-react';
const SportRenderer = (props) => {
return (
<i
className="far fa-trash-alt"
style={{ cursor: 'pointer' }}
onClick={() => props.api.applyTransaction({ remove: [props.node.data] })}
></i>
);
};
const leftColumns = [
{
rowDrag: true,
maxWidth: 50,
suppressMenu: true,
rowDragText: (params, dragItemCount) => {
if (dragItemCount > 1) {
return dragItemCount + ' athletes';
}
return params.rowNode.data.athlete;
},
},
{
colId: 'checkbox',
maxWidth: 50,
checkboxSelection: true,
suppressMenu: true,
headerCheckboxSelection: true,
},
{ field: 'athlete' },
{ field: 'sport' },
];
const rightColumns = [
{
rowDrag: true,
maxWidth: 50,
suppressMenu: true,
rowDragText: (params, dragItemCount) => {
if (dragItemCount > 1) {
return dragItemCount + ' athletes';
}
return params.rowNode.data.athlete;
},
},
{ field: 'athlete' },
{ field: 'sport' },
{
suppressMenu: true,
maxWidth: 50,
cellRenderer: SportRenderer,
},
];
const defaultColDef = {
flex: 1,
minWidth: 100,
sortable: true,
filter: true,
resizable: true,
};
const GridExample = () => {
const [leftApi, setLeftApi] = useState(null);
const [leftColumnApi, setLeftColumnApi] = useState(null);
const [rightApi, setRightApi] = useState(null);
const [rawData, setRawData] = useState([]);
const [leftRowData, setLeftRowData] = useState(null);
const [rightRowData, setRightRowData] = useState([]);
const [radioChecked, setRadioChecked] = useState(0);
const [checkBoxSelected, setCheckBoxSelected] = useState(true);
useEffect(() => {
if (!rawData.length) {
fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
.then((resp) => resp.json())
.then((data) => {
const athletes = [];
let i = 0;
while (athletes.length < 20 && i < data.length) {
var pos = i++;
if (athletes.some((rec) => rec.athlete === data[pos].athlete)) {
continue;
}
athletes.push(data[pos]);
}
setRawData(athletes);
});
}
}, [rawData]);
const loadGrids = useCallback(() => {
setLeftRowData([...rawData]);
setRightRowData([]);
leftApi.deselectAll();
}, [leftApi, rawData]);
useEffect(() => {
if (rawData.length) {
loadGrids();
}
}, [rawData, loadGrids]);
useEffect(() => {
if (leftColumnApi && leftApi) {
leftColumnApi.setColumnVisible('checkbox', checkBoxSelected);
leftApi.setSuppressRowClickSelection(checkBoxSelected);
}
}, [leftColumnApi, leftApi, checkBoxSelected]);
const reset = () => {
setRadioChecked(0);
setCheckBoxSelected(true);
loadGrids();
};
const onRadioChange = (e) => {
setRadioChecked(parseInt(e.target.value, 10));
};
const onCheckboxChange = (e) => {
const checked = e.target.checked;
setCheckBoxSelected(checked);
};
const getRowId = (params) => params.data.athlete;
const onDragStop = useCallback(
(params) => {
var nodes = params.nodes;
if (radioChecked === 0) {
leftApi.applyTransaction({
remove: nodes.map(function (node) {
return node.data;
}),
});
} else if (radioChecked === 1) {
leftApi.setNodesSelected({ nodes, newValue: false });
}
},
[leftApi, radioChecked]
);
useEffect(() => {
if (!leftApi || !rightApi) {
return;
}
const dropZoneParams = rightApi.getRowDropZoneParams({ onDragStop });
leftApi.removeRowDropZone(dropZoneParams);
leftApi.addRowDropZone(dropZoneParams);
}, [leftApi, rightApi, onDragStop]);
const onGridReady = (params, side) => {
if (side === 0) {
setLeftApi(params.api);
setLeftColumnApi(params.columnApi);
}
if (side === 1) {
setRightApi(params.api);
}
};
const getTopToolBar = () => (
<div className="example-toolbar panel panel-default">
<div className="panel-body">
<div onChange={onRadioChange}>
<input
type="radio"
id="move"
name="radio"
value="0"
checked={radioChecked === 0}
/>{' '}
<label htmlFor="move">Remove Source Rows</label>
<input
type="radio"
id="deselect"
name="radio"
value="1"
checked={radioChecked === 1}
/>{' '}
<label htmlFor="deselect">Only Deselect Source Rows</label>
<input
type="radio"
id="none"
name="radio"
value="2"
checked={radioChecked === 2}
/>{' '}
<label htmlFor="none">None</label>
</div>
<input
type="checkbox"
id="toggleCheck"
checked={checkBoxSelected}
onChange={onCheckboxChange}
/>
<label htmlFor="toggleCheck">Checkbox Select</label>
<span className="input-group-button">
<button
type="button"
className="btn btn-default reset"
style={{ marginLeft: '5px' }}
onClick={reset}
>
<i className="fas fa-redo" style={{ marginRight: '5px' }}></i>Reset
</button>
</span>
</div>
</div>
);
const getGridWrapper = (id) => (
<div className="panel panel-primary" style={{ marginRight: '10px' }}>
<div className="panel-heading">
{id === 0 ? 'Athletes' : 'Selected Athletes'}
</div>
<div className="panel-body">
<AgGridReact
style={{ height: '100%' }}
defaultColDef={defaultColDef}
getRowId={getRowId}
rowDragManaged={true}
animateRows={true}
rowSelection={id === 0 ? 'multiple' : undefined}
rowDragMultiRow={id === 0}
suppressRowClickSelection={id === 0}
suppressMoveWhenRowDragging={id === 0}
rowData={id === 0 ? leftRowData : rightRowData}
columnDefs={id === 0 ? leftColumns : rightColumns}
onGridReady={(params) => onGridReady(params, id)}
/>
</div>
</div>
);
return (
<div className="top-container">
{getTopToolBar()}
<div className="grid-wrapper ag-theme-alpine">
{getGridWrapper(0)}
{getGridWrapper(1)}
</div>
</div>
);
};
const root = createRoot(document.getElementById('root'));
root.render(<GridExample />);
I would greatly appreciate any guidance, code examples, or best practices that can help me achieve the desired behavior. Thank you for your time and assistance!
In the answer below, I have used the data coming from this api: https://www.ag-grid.com/example-assets/olympic-winners.json
I have used "Country" as group column instead of "Gender", because there is no such thing in the response body. Please tweak your code accordingly.
Question 1
"How can I display the "Male" and "Female" categories in the right grid by default, even when there is no data available in either grid?"
Answer
We should show a dummy data in which only the country field has a value.
In this example, I am doing that for the right grid. Once we get the data, we group our data by country and get our dummy data ready.
We'll get something like this in the end
{
country: 'United States',
athlete: null,
gold: null,
silver: null,
bronze: null,
},
{
country: 'Russia',
athlete: null,
gold: null,
silver: null,
bronze: null,
}
To do that, we implement a new method
const getRightGridData = useCallback((data) => {
var dataGroupedByCountry = data.reduce(function (r, a) {
r[a.country] = r[a.country] || [];
r[a.country].push(a);
return r;
}, Object.create(null));
var countries = Object.keys(dataGroupedByCountry);
var data = [];
for (let i = 0; i < countries.length; i++) {
data.push({
country: countries[i],
athlete: null,
gold: null,
silver: null,
bronze: null,
});
}
return data;
}, []);
and use it in onGridReady event of the left grid
const onLeftGridReady = (params) => {
fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
.then((resp) => resp.json())
.then((data) => {
console.log('data', data);
var rightGridData = getRightGridData(data);
rightGridRef.current.api.setRowData(rightGridData);
var leftGridData = getLeftGridData(data);
params.api.setRowData(leftGridData);
});
};
Similarly, we should get the left grid's data ready
const getLeftGridData = useCallback((data) => {
const result = [
...data
.reduce((r, o) => {
const key = o.athlete + '-' + o.country;
const item =
r.get(key) ||
Object.assign({}, o, {
gold: 0,
silver: 0,
bronze: 0,
});
item.gold += o.gold;
item.silver += o.silver;
item.bronze += o.bronze;
return r.set(key, item);
}, new Map())
.values(),
];
// we do not need these fields
delete result.age;
delete result.date;
return result;
}, []);
Now, we have this scene in the beginning
Question 2
"When dragging rows from the left grid to the right grid, how can I ensure that the dropped rows are automatically placed under the appropriate default category (e.g., "Male" category for male athletes) in the right grid?"
Answer
As I said earlier, I take "Country" here as group column. Whenever we drag and drop a column, it should be placed under the right country.
We want to make sure that we cannot drag and drop the same row. We also want to make sure that the dummy data disappears right after our first drag and drop action.
const gridDrop = (grid, event) => {
console.log('gridDrop');
event.preventDefault();
const jsonData = event.dataTransfer.getData('application/json');
const data = JSON.parse(jsonData);
console.log('bu data', data);
// if data missing or data has no it, do nothing
if (!data || data.country == null || data.athlete == null) {
return;
}
const gridApi =
grid === 'left' ? leftGridRef.current.api : rightGridRef.current.api;
// do nothing if row is already in the grid, otherwise we would have duplicates
const rowAlreadyInGrid = !!gridApi.getRowNode(
data.country + '-' + data.athlete
);
if (rowAlreadyInGrid) {
console.log('not adding row to avoid duplicates in the grid');
return;
}
// if it is our first drag and drop action, remove the dummy data
var removeAll = true;
rightGridRef.current.api.forEachNode((node) => {
console.log('node', node);
if (node.group == false && node.data.athlete !== null) removeAll = false;
});
if (removeAll) gridApi.setRowData([]);
// add data
const transaction = {
add: [data],
};
gridApi.applyTransaction(transaction);
};
Here's what we get after our first drag and drop action
Of course, base requirements for drag and drop actions should be set. But if I try to share all the code, it will be a very long answer. Please check the working plunker below.
https://plnkr.co/edit/ZfhOKRI5PlQRDwxU?preview
UPDATE
If you want group columns to stay even if there is no real data under it, we can modify our code like below
const gridDrop = (grid, event) => {
console.log('gridDrop');
event.preventDefault();
const jsonData = event.dataTransfer.getData('application/json');
const data = JSON.parse(jsonData);
console.log('bu data', data);
// if data missing or data has no it, do nothing
if (!data || data.country == null || data.athlete == null) {
return;
}
const gridApi =
grid === 'left' ? leftGridRef.current.api : rightGridRef.current.api;
// do nothing if row is already in the grid, otherwise we would have duplicates
const rowAlreadyInGrid = !!gridApi.getRowNode(
data.country + '-' + data.athlete
);
if (rowAlreadyInGrid) {
console.log('not adding row to avoid duplicates in the grid');
return;
}
// we check if we have dummy data to delete here
var removeAll = true;
rightGridRef.current.api.forEachNode((node) => {
console.log('node', node);
if (
node.group == false &&
node.data.country == data.country &&
node.data.athlete !== null
)
removeAll = false;
});
// if we have dummy data, mark it as to be removed
const itemsToDelete = [];
if (removeAll) {
rightGridRef.current.api.forEachNode((node) => {
if (node.group == false && node.data.country == data.country) {
const item = node.data;
itemsToDelete.push(item);
}
});
}
// add current data
const transaction = {
add: [data],
};
gridApi.applyTransaction(transaction);
// remove dummy data if exists
gridApi.applyTransaction({ remove: itemsToDelete });
};
This is what we get after our first drag and drop action
Modified plunker: https://plnkr.co/edit/xTuOz4lasQGvQbCw?preview