행 드래그
행을 끌어 순서를 바꾸고, 트리에서 다른 부모로, 다른 그리드로 옮깁니다. canDrop 규칙, 실행 취소, 순서 저장용 이벤트가 있습니다.
실행 예제행 드래그
행을 다른 부모나 다른 그리드로 옮기고, 펼칠 때 자식을 불러오고, 두 그리드를 함께 스크롤합니다.
이 데모의 코드
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를 켜면 손잡이 컬럼이 생겨 우선순위나 작업 순서처럼 행 순서를 직접 바꿀 수 있습니다. 손잡이로 행을 끌어 두 행 사이에 놓으면 원본 데이터 순서가 바뀌고, Ctrl+Z로 되돌릴 수 있으며, rowDragEnd 이벤트가 이전 위치와 새 위치를 알려 주므로 순서를 저장하면 됩니다. 정렬·필터·그룹이 걸려 있으면 화면 순서와 데이터 순서가 달라서 끌기가 잠시 꺼집니다.
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)다른 부모, 다른 그리드로
트리 데이터에서 treeData.setParentId를 주면 행을 다른 부모 아래로 옮길 수 있습니다. 행의 위쪽이나 아래쪽 가장자리에 놓으면 형제가 되고, 가운데에 놓으면 자식이 됩니다. rowDrag에 같은 group을 준 그리드끼리는 작업 목록과 백로그처럼 행을 주고받을 수 있고, 행을 받은 그리드에서 rowDrop이 발생합니다. canDrop으로 폴더가 아닌 행 안에 놓는 것처럼 놓을 수 없는 곳을 막습니다. 두 그리드를 함께 스크롤하려면 syncScroll()을 쓰세요. 다른 그리드로 복사하기, 복사본에 새 id 주기, 놓기 규칙은 그리드 간 행 드래그에 있습니다.
// 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');옵션, 메서드, 이벤트
| 이름 | 타입 | 설명 |
|---|---|---|
rowDrag | boolean | { group?, canDrop? } | 행을 끄는 손잡이 컬럼. 정렬·필터·그룹이 걸려 있는 동안에는 잠시 꺼집니다. |
rowDrag.group | string | group이 같은 그리드끼리 행을 주고받습니다. |
rowDrag.canDrop({ row, target, position, fromOtherGrid }) | boolean | false를 돌려주면 그곳에 놓을 수 없습니다. position은 'before', 'after', 트리에서는 'inside'. |
moveRow(row, toIndex) / dropRow(row, target, position?) | boolean | 코드로 행을 옮깁니다(실행 취소 가능). 거부되거나 정렬·필터·그룹 중이면 false. |
syncScroll(otherGrid, axis?) | () => void | 두 그리드를 함께 스크롤합니다('both', 'vertical', 'horizontal'). 연결을 끊는 함수를 돌려줍니다. |
rowDragEnd / rowDrop | events | rowDragEnd { row, fromIndex, toIndex }는 그리드 안에서 옮긴 뒤, rowDrop { row, target, position, fromOtherGrid, fromGrid }은 행을 받은 그리드에서 발생합니다. |
이 페이지에 대한 질문
행을 끌어서 순서를 바꿀 수 있나요?
네. rowDrag: true를 주면 끌기 손잡이가 생기고 rowDragEnd가 새 위치를 알려 줍니다. group 이름을 주면 그리드끼리, 트리에서는 다른 부모로도 옮길 수 있습니다.