FreeGridv1.15.1한국어

Row drag

Drag rows into a new order, to another parent in a tree, or to another grid, with canDrop rules, undo and events to save the new order.

Live demoRow drag

Drag rows to another parent or another grid, load tree children on expand, scroll two grids together.

Code for this demo
import { createGrid, type Row } from '@freegridjs/core';
import { separator, text, toggle, tr, type DemoContext } from '../ui';

export default function mount(ctx: DemoContext) {
  const { lang } = ctx;
  const h = (en: string, ko: string) => tr(lang, { en, ko });
  // two grids side by side
  ctx.host.style.display = 'grid';
  ctx.host.style.gridTemplateColumns = 'repeat(auto-fit, minmax(300px, 1fr))';
  ctx.host.style.gap = '16px';
  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.style.gridAutoRows = '100%';
  ctx.host.append(left, right);

  const tasks: Row[] = [
    { id: 1, name: h('Website launch', '웹사이트 오픈'), parent: null, folder: true },
    { id: 2, name: h('Design', '디자인'), parent: 1, folder: true },
    { id: 3, name: h('Build', '개발'), parent: 1, folder: true },
    { id: 4, name: h('Logo', '로고'), parent: 2 },
    // children of this one come from the "server" on first expand
    { id: 5, name: h('Archive (loads on expand)', '보관함 (펼칠 때 불러옴)'), parent: null, folder: true, lazy: true },
  ];
  let nextId = 100;
  const columns = [{ field: 'name', header: h('Tasks', '작업'), flex: 1 }];

  const tree = createGrid(left, {
    columns, data: tasks, locale: lang, getRowId: r => String(r.id), groupDefaultExpanded: -1,
    rowDrag: {
      group: 'tasks',
      // only folders take children
      canDrop: ({ target, position }) => position !== 'inside' || !!target?.folder,
    },
    treeData: {
      getParentId: r => r.parent,
      setParentId: (row, parentId) => { row.parent = parentId; },
      hasChildren: r => !!r.lazy,
      // pretend server: answers after 600 ms
      loadChildren: row => new Promise<Row[]>(resolve => setTimeout(() => resolve([
        { id: nextId++, name: h('Old banner', '예전 배너'), parent: row.id },
        { id: nextId++, name: h('2025 report', '2025 보고서'), parent: row.id },
      ]), 600)),
    },
  });
  const backlog = createGrid(right, {
    columns: [{ field: 'name', header: h('Backlog', '백로그'), flex: 1 }],
    data: Array.from({ length: 30 }, (_, i) => ({ id: 200 + i, name: `${h('Idea', '아이디어')} ${i + 1}`, parent: null })),
    locale: lang, getRowId: r => String(r.id),
    rowDrag: { group: 'tasks' },
  });

  for (const [g, name] of [[tree, h('tasks', '작업')], [backlog, h('backlog', '백로그')]] as const) {
    g.on('rowDrop', e => ctx.log(`rowDrop (${name}) → "${e.row.name}" ${e.position} ${e.target ? `"${e.target.name}"` : h('end', '끝')}${e.fromOtherGrid ? h(' from the other grid', ' (다른 그리드에서)') : ''}`));
  }
  tree.on('childrenLoaded', e => ctx.log(`childrenLoaded → ${e.rows.length}`));

  text(ctx, h('Drag a row by its handle: onto the middle of a folder to move it inside, onto an edge to put it before or after. Drag between the two grids too.',
    '손잡이로 행을 끌어 보세요. 폴더 가운데에 놓으면 그 안으로, 위·아래 가장자리에 놓으면 앞이나 뒤로 갑니다. 두 그리드 사이로도 옮길 수 있습니다.'));
  separator(ctx);
  let unlink: (() => void) | null = null;
  toggle(ctx, h('Scroll both grids together', '두 그리드 함께 스크롤'), false, on => {
    if (on) unlink = tree.syncScroll(backlog, 'vertical');
    else unlink?.();
    ctx.log(`syncScroll: ${on}`);
  });
  return tree;
}

rowDrag: true adds a handle column for putting rows in order by hand, such as priorities or the order of steps. Drag a row by its handle and drop it between two rows; the source data is reordered, the move can be undone with Ctrl+Z, and rowDragEnd tells you the old and new position so you can save the order. Dragging pauses while the rows are sorted, filtered or grouped, because the order on screen is then not the order of the data.

const grid = createGrid(el, { columns, data, getRowId: r => r.id, rowDrag: true });

grid.on('rowDragEnd', e => saveOrder(e.row, e.toIndex));   // { row, fromIndex, toIndex }

// the same from code (undoable); false while sorted, filtered or grouped
grid.moveRow(rows[5], 0);
grid.dropRow(rows[5], rows[2], 'after');   // 'before' | 'after' | 'inside' (tree data)

Other parents and other grids

In tree data with treeData.setParentId, rows can move to another parent: drop on the top or bottom edge of a row to make a sibling, on its middle to make a child. Grids whose rowDrag share a group exchange rows, for example a task list and a backlog; the receiving grid fires rowDrop. canDrop refuses targets, such as dropping inside a row that is not a folder. syncScroll() keeps two grids scrolled together. Copying rows into the other grid, giving the copy its own id and the rules for drops are on Dragging rows between grids.

// drag rows to another parent, and between two grids
const files = createGrid(left, {
  columns, data: roots, getRowId: r => r.id,
  rowDrag: { group: 'files', canDrop: ({ target, position }) => position !== 'inside' || target?.folder },
  treeData: {
    getParentId: r => r.parent,
    setParentId: (row, parentId) => { row.parent = parentId; },   // needed to move rows to another parent
  },
});
// another grid in the same group: rows can be dragged between them
const archive = createGrid(right, { columns, data: [], getRowId: r => r.id, rowDrag: { group: 'files' } });
archive.on('rowDrop', e => save(e.row, e.fromOtherGrid));

// keep two grids scrolled together
const unlink = files.syncScroll(archive, 'vertical');

Options, methods and events

NameTypeDescription
rowDragboolean | { group?, canDrop? }A handle column for dragging rows. It pauses while the rows are sorted, filtered or grouped.
rowDrag.groupstringGrids with the same group accept rows dragged between them.
rowDrag.canDrop({ row, target, position, fromOtherGrid })booleanReturn false to refuse a target. position is 'before', 'after' or (tree data) 'inside'.
moveRow(row, toIndex) / dropRow(row, target, position?)booleanMove a row from code (undoable); false when refused or while sorted, filtered or grouped.
syncScroll(otherGrid, axis?)() => voidScroll two grids together ('both', 'vertical' or 'horizontal'); returns a function that unlinks them.
rowDragEnd / rowDropeventsrowDragEnd { row, fromIndex, toIndex } after a move inside the grid; rowDrop { row, target, position, fromOtherGrid, fromGrid } on the grid that received the row.

Questions about this page

Can users reorder rows by dragging?

Yes. rowDrag: true adds a drag handle; rowDragEnd reports the new position. With a group name rows can also move between grids, and in tree data to another parent.