FreeGridv1.15.1한국어

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));
NameTypeDescription
groupstringGrids with the same group exchange rows.
copyboolean | '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) => RowThe 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) => booleanSet on the receiving grid. params: row, target, position, fromOtherGrid, copy. Return false to refuse the place.

Events and undo

  • The receiving grid fires rowDrop with fromOtherGrid: true, fromGrid (the other grid) and copy when it was copied. A moved row also fires rowsRemoved in the grid it left and rowsAdded in 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 copy may 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.