Dragging rows between grids
Grids that share a rowDrag group exchange rows: a drop moves the row, rowDrag.copy (or Ctrl while dropping) copies it, copyRow gives the copy a new id, canDrop refuses targets.
Live demoRows between grids
Copy products into an order and reorder its lines.
Code for this demo
import { createGrid, type GridApi, type Row } from '@freegridjs/core';
import { button, separator, text, tr, type DemoContext } from '../ui';
export default function mount(ctx: DemoContext) {
const h = (en: string, ko: string) => tr(ctx.lang, { en, ko });
ctx.host.style.display = 'grid';
ctx.host.style.gridTemplateColumns = 'repeat(auto-fit, minmax(300px, 1fr))';
ctx.host.style.gap = '16px';
ctx.host.style.gridAutoRows = '100%';
const left = document.createElement('div');
const right = document.createElement('div');
for (const el of [left, right]) el.style.cssText = 'height: 100%; min-height: 0';
ctx.host.append(left, right);
const names = h('Keyboard,Mouse,Monitor,Laptop stand,USB-C hub,Webcam,Headset,Desk lamp,Chair,Notebook', '키보드,마우스,모니터,노트북 거치대,USB-C 허브,웹캠,헤드셋,스탠드,의자,노트').split(',');
const catalogue: Row[] = names.map((name, i) => ({ id: `p${i + 1}`, name, price: 19000 + i * 7000 }));
let next = 1;
const products = createGrid(left, {
columns: [{ field: 'name', header: h('Products (copied out)', '상품 (복사됨)'), flex: 1 }, { field: 'price', header: h('Price', '가격'), type: 'number', format: '#,##0', width: 100 }],
data: catalogue, locale: ctx.lang, getRowId: r => String(r.id),
rowDrag: {
group: 'order',
copy: true,
// the order line gets its own id and a quantity
copyRow: p => ({ id: `line${next++}`, productId: p.id, name: p.name, price: p.price, qty: 1 }),
},
});
// canDrop reads this grid, so its type is written out
const order: GridApi = createGrid(right, {
columns: [
{ field: 'name', header: h('Order', '주문'), flex: 1 },
{ field: 'qty', header: h('Qty', '수량'), type: 'number', width: 70, editable: true },
{ id: 'amount', header: h('Amount', '금액'), type: 'number', format: '#,##0', width: 100, valueGetter: r => r.price * r.qty },
],
data: [], locale: ctx.lang, getRowId: r => String(r.id),
rowDrag: {
group: 'order',
// a product goes in once; moving lines inside the order is fine
canDrop: p => !p.fromOtherGrid || !order.getData().some(r => r.productId === p.row.id),
},
noRowsOverlay: h('Drag products here', '상품을 여기로 끌어 오세요'),
});
order.on('rowDrop', e => ctx.log(e.fromOtherGrid ? `${e.copy ? h('copied', '복사') : h('moved', '이동')}: ${e.row.name}` : `${h('reordered', '순서 변경')}: ${e.row.name}`));
text(ctx, h('Drag a product by its handle into the order: it is copied, and the same product is refused a second time. Drag order lines to reorder them.',
'손잡이로 상품을 주문에 끌어 놓으면 복사되고, 같은 상품은 두 번 들어가지 않습니다. 주문 줄은 끌어서 순서를 바꿉니다.'));
separator(ctx);
button(ctx, h('Order total', '주문 합계'), () => ctx.log(`${order.getData().reduce((s, r) => s + r.price * r.qty, 0).toLocaleString()}`));
button(ctx, h('Undo in order', '주문에서 실행 취소'), () => order.undo());
return products;
}Two lists side by side — a product catalogue and an order, a backlog and this week's tasks — often need rows moved or copied from one to the other. Give both grids rowDrag with the same group, and a row dragged by its handle can be dropped between the rows of the other grid.
const backlog = createGrid(left, { columns, data: tasks, getRowId: r => String(r.id), rowDrag: { group: 'tasks' } });
const thisWeek = createGrid(right, { columns, data: [], getRowId: r => String(r.id), rowDrag: { group: 'tasks' } });
thisWeek.on('rowDrop', e => {
if (e.fromOtherGrid) save({ id: e.row.id, list: 'this-week' });
});Copy instead of move
With copy: true on the grid the rows come from, a drop into another grid adds a copy there and leaves the row where it was — a catalogue stays complete while products go into orders. Without it, people can still copy by holding Ctrl (⌘ on a Mac) while they drop; copy: 'ctrl' makes that the only way, and copy: false turns copying off. A + on the drop line shows that the row will be copied.
const products = createGrid(left, {
columns, data: catalogue, getRowId: r => String(r.id),
rowDrag: {
group: 'order',
copy: true,
// the copy gets its own id and a quantity; nested objects are copied too
copyRow: p => ({ id: crypto.randomUUID(), productId: p.id, name: p.name, price: p.price, qty: 1 }),
},
});
// canDrop reads the grid itself, so give it a type (TypeScript cannot infer it)
const order: GridApi = createGrid(right, {
columns: orderColumns, data: [], getRowId: r => String(r.id),
rowDrag: {
group: 'order',
// only products come in, and never twice
canDrop: p => p.fromOtherGrid && !order.getData().some(r => r.productId === p.row.id),
},
});
order.on('rowDrop', e => console.log(e.copy ? 'added' : 'moved', e.row));| Name | Type | Description |
|---|---|---|
group | string | Grids with the same group exchange rows. |
copy | boolean | 'ctrl' | Set on the grid rows come from. true: dropping into another grid copies; 'ctrl': only with Ctrl / ⌘ held; false: never. Default: Ctrl / ⌘ copies, a plain drop moves. |
copyRow | (row) => Row | The copy that goes into the other grid. Default: the row's values, nested objects copied too. Give the copy a new id here when the two grids share ids: with getRowId, a row whose id the grid already has is not dropped (the default copy keeps the id, so a row copies in once). |
canDrop | (params) => boolean | Set on the receiving grid. params: row, target, position, fromOtherGrid, copy. Return false to refuse the place. |
Events and undo
- The receiving grid fires
rowDropwithfromOtherGrid: true,fromGrid(the other grid) andcopywhen it was copied. A moved row also firesrowsRemovedin the grid it left androwsAddedin the new one. - Each grid records its own part: Ctrl+Z in the receiving grid takes the row out again; in the grid it left, Ctrl+Z puts it back.
- Added and removed rows count as changes (
getModifiedRows), so a save button sees them.
When a drop is not possible
- A grid that is sorted, filtered or grouped does not take rows in, because the place on screen is not a place in the data. A grid with
copymay still be dragged from while sorted or filtered. - A row already in the receiving grid (the same object) is not taken twice.
- Server data (datasource) does not take part: change the server and send a transaction.
Reordering within one grid and tree data are on Row drag.
Questions about this page
How do I drag a row from one grid to another?
Turn on rowDrag in both with the same group: rowDrag: { group: 'tasks' }. Drag a row by its handle into the other grid; it moves there and the receiving grid fires rowDrop.
Can the row stay in the first grid (copy instead of move)?
Yes: rowDrag.copy: true on the grid the rows come from, or hold Ctrl (⌘ on a Mac) while dropping. copyRow lets you give the copy its own id.