LyteNyte Grid provides flexible row dragging that lets users move data through intuitive drag-and-drop interactions. You can drag single or multiple rows within a grid, across multiple grids, or into external drop zones. The grid supplies the mechanics without enforcing default behavior, so you control how drag interactions are handled.
Use the grid.api.useRowDrag
hook to add row dragging. This hook returns drag props that you attach
to a drag handle component. It expects a getDragData
callback, which must return the drag data in
the following shape:
export interface DragData {
readonly siteLocalData?: Record<string, any>;
readonly dataTransfer?: Record<string, string>;
}
Drop zones must declare which drag data keys they accept. For example, the Row
component is a
dropzone. Set its accepted
property to specify what it will accept.
The example below shows a basic setup. Pay attention to the marker column's cellRenderer
and the
accepted
prop on Grid.Row
.
"use client";
import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";
import "@1771technologies/lytenyte-pro/grid.css";
import { DragDotsSmallIcon } from "@1771technologies/lytenyte-pro/icons";
import type { Column } from "@1771technologies/lytenyte-pro/types";
import { bankDataSmall } from "@1771technologies/sample-data/bank-data-smaller";
import { useId } from "react";
type BankData = (typeof bankDataSmall)[number];
const columns: Column<BankData>[] = [
{ id: "education" },
{ id: "marital" },
{ id: "age", type: "number" },
{ id: "job" },
{ id: "balance", type: "number" },
{ id: "default" },
{ id: "housing" },
{ id: "loan" },
{ id: "contact" },
{ id: "day", type: "number" },
{ id: "month" },
{ id: "duration" },
{ id: "campaign" },
{ id: "pdays" },
{ id: "previous" },
{ id: "poutcome" },
{ id: "y" },
];
export default function RowDragging() {
const ds = useClientRowDataSource({
data: bankDataSmall,
});
const grid = Grid.useLyteNyte({
gridId: useId(),
rowDataSource: ds,
columns,
columnMarkerEnabled: true,
columnMarker: {
cellRenderer: (p) => {
const drag = p.grid.api.useRowDrag({
getDragData: () => {
return {
siteLocalData: {
row: p.rowIndex,
},
};
},
placeholder: () => {
return (
<div className="flex p-2 rounded bg-black text-white">
Dragging row at index: {p.rowIndex}
</div>
);
},
onDrop: (p) => {
alert(
`Dropped row at ${p.state.siteLocalData?.row ?? ""} ${
p.moveState.topHalf ? "before" : "after"
} row ${p.dropElement.getAttribute("data-ln-rowindex")}`
);
},
});
return (
<span {...drag.dragProps}>
<DragDotsSmallIcon />
</span>
);
},
},
});
const view = grid.view.useValue();
return (
<div className="lng-grid" style={{ height: 500 }}>
<Grid.Root grid={grid}>
<Grid.Viewport>
<Grid.Header>
{view.header.layout.map((row, i) => {
return (
<Grid.HeaderRow key={i} headerRowIndex={i}>
{row.map((c) => {
if (c.kind === "group") return null;
return (
<Grid.HeaderCell
key={c.id}
cell={c}
className="flex w-full h-full capitalize px-2 items-center"
/>
);
})}
</Grid.HeaderRow>
);
})}
</Grid.Header>
<Grid.RowsContainer>
<Grid.RowsCenter>
{view.rows.center.map((row) => {
if (row.kind === "full-width") return null;
return (
<Grid.Row row={row} key={row.id} accepted={["row"]}>
{row.cells.map((c) => {
return (
<Grid.Cell
key={c.id}
cell={c}
className="text-sm flex items-center px-2 h-full w-full"
/>
);
})}
</Grid.Row>
);
})}
</Grid.RowsCenter>
</Grid.RowsContainer>
</Grid.Viewport>
</Grid.Root>
</div>
);
}
Extend row dragging to multiple rows by combining it with Row Selection. Instead of dragging a single
row, you query the selection state in getDragData
to include all selected rows.
"use client";
import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";
import "@1771technologies/lytenyte-pro/grid.css";
import { DragDotsSmallIcon } from "@1771technologies/lytenyte-pro/icons";
import type { Column } from "@1771technologies/lytenyte-pro/types";
import { bankDataSmall } from "@1771technologies/sample-data/bank-data-smaller";
import { useId } from "react";
type BankData = (typeof bankDataSmall)[number];
const columns: Column<BankData>[] = [
{ id: "education" },
{ id: "marital" },
{ id: "age", type: "number" },
{ id: "job" },
{ id: "balance", type: "number" },
{ id: "default" },
{ id: "housing" },
{ id: "loan" },
{ id: "contact" },
{ id: "day", type: "number" },
{ id: "month" },
{ id: "duration" },
{ id: "campaign" },
{ id: "pdays" },
{ id: "previous" },
{ id: "poutcome" },
{ id: "y" },
];
export default function RowDraggingMultiple() {
const ds = useClientRowDataSource({
data: bankDataSmall,
});
const grid = Grid.useLyteNyte({
gridId: useId(),
rowDataSource: ds,
columns,
rowSelectionMode: "multiple",
rowSelectionActivator: "single-click",
columnMarkerEnabled: true,
columnMarker: {
cellRenderer: (p) => {
const drag = p.grid.api.useRowDrag({
getDragData: () => {
const allIndices = [...grid.state.rowSelectedIds.get()]
.map((c) => {
return grid.api.rowById(c)?.id;
})
.filter((c) => c != null);
return {
siteLocalData: {
row: [...new Set([...allIndices, p.row.id])],
},
};
},
placeholder: () => {
return (
<div className="flex p-2 rounded bg-black text-white">
Dragging rows
</div>
);
},
onDrop: (p) => {
alert(
`Dropped rows at indices ${p.state.siteLocalData?.row?.join(
", "
)} ${
p.moveState.topHalf ? "before" : "after"
} row ${p.dropElement.getAttribute("data-ln-rowindex")}`
);
},
});
return (
<span {...drag.dragProps}>
<DragDotsSmallIcon />
</span>
);
},
},
});
const view = grid.view.useValue();
return (
<div className="lng-grid" style={{ height: 500 }}>
<Grid.Root grid={grid}>
<Grid.Viewport>
<Grid.Header>
{view.header.layout.map((row, i) => {
return (
<Grid.HeaderRow key={i} headerRowIndex={i}>
{row.map((c) => {
if (c.kind === "group") return null;
return (
<Grid.HeaderCell
key={c.id}
cell={c}
className="flex w-full h-full capitalize px-2 items-center"
/>
);
})}
</Grid.HeaderRow>
);
})}
</Grid.Header>
<Grid.RowsContainer>
<Grid.RowsCenter>
{view.rows.center.map((row) => {
if (row.kind === "full-width") return null;
return (
<Grid.Row row={row} key={row.id} accepted={["row"]}>
{row.cells.map((c) => {
return (
<Grid.Cell
key={c.id}
cell={c}
className="text-sm flex items-center px-2 h-full w-full"
/>
);
})}
</Grid.Row>
);
})}
</Grid.RowsCenter>
</Grid.RowsContainer>
</Grid.Viewport>
</Grid.Root>
</div>
);
}
To drag between grids, use a shared accepted
value. For example, setting accepted=["row"]
on both
grids allows any drag data with a row
property to be dropped onto rows in either grid.
"use client";
import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";
import "@1771technologies/lytenyte-pro/grid.css";
import { DragDotsSmallIcon } from "@1771technologies/lytenyte-pro/icons";
import type { Column } from "@1771technologies/lytenyte-pro/types";
import { bankDataSmall } from "@1771technologies/sample-data/bank-data-smaller";
import { useId } from "react";
type BankData = (typeof bankDataSmall)[number];
const columns: Column<BankData>[] = [
{ id: "education" },
{ id: "marital" },
{ id: "age", type: "number" },
{ id: "job" },
{ id: "balance", type: "number" },
{ id: "default" },
{ id: "housing" },
{ id: "loan" },
{ id: "contact" },
{ id: "day", type: "number" },
{ id: "month" },
{ id: "duration" },
{ id: "campaign" },
{ id: "pdays" },
{ id: "previous" },
{ id: "poutcome" },
{ id: "y" },
];
export default function RowDraggingBetweenGrids() {
const ds = useClientRowDataSource({
data: bankDataSmall,
});
const upper = Grid.useLyteNyte({
gridId: useId(),
rowDataSource: ds,
columns,
columnMarkerEnabled: true,
columnMarker: {
cellRenderer: (p) => {
const drag = p.grid.api.useRowDrag({
getDragData: () => {
return {
siteLocalData: {
row: p.rowIndex,
},
};
},
placeholder: () => {
return (
<div className="flex p-2 rounded bg-black text-white">
Dragging row at index: {p.rowIndex}
</div>
);
},
onDrop: (p) => {
alert(
`Dropped row at ${p.state.siteLocalData?.row ?? ""} ${
p.moveState.topHalf ? "before" : "after"
} row ${p.dropElement.getAttribute("data-ln-rowindex")}`
);
},
});
return (
<span {...drag.dragProps}>
<DragDotsSmallIcon />
</span>
);
},
},
});
const lower = Grid.useLyteNyte({
gridId: useId(),
rowDataSource: ds,
columns,
columnMarkerEnabled: true,
columnMarker: {
cellRenderer: (p) => {
const drag = p.grid.api.useRowDrag({
getDragData: () => {
return {
siteLocalData: {
row: p.rowIndex,
},
};
},
placeholder: () => {
return (
<div className="flex p-2 rounded bg-black text-white">
Dragging row at index: {p.rowIndex}
</div>
);
},
onDrop: (p) => {
alert(
`Dropped row at ${p.state.siteLocalData?.row ?? ""} ${
p.moveState.topHalf ? "before" : "after"
} row ${p.dropElement.getAttribute("data-ln-rowindex")}`
);
},
});
return (
<span {...drag.dragProps}>
<DragDotsSmallIcon />
</span>
);
},
},
});
const viewUpper = upper.view.useValue();
const viewLower = lower.view.useValue();
return (
<div className="flex flex-col gap-8">
<div className="lng-grid" style={{ height: 200 }}>
<Grid.Root grid={upper}>
<Grid.Viewport>
<Grid.Header>
{viewUpper.header.layout.map((row, i) => {
return (
<Grid.HeaderRow key={i} headerRowIndex={i}>
{row.map((c) => {
if (c.kind === "group") return null;
return (
<Grid.HeaderCell
key={c.id}
cell={c}
className="flex w-full h-full capitalize px-2 items-center"
/>
);
})}
</Grid.HeaderRow>
);
})}
</Grid.Header>
<Grid.RowsContainer>
<Grid.RowsCenter>
{viewUpper.rows.center.map((row) => {
if (row.kind === "full-width") return null;
return (
<Grid.Row row={row} key={row.id} accepted={["row"]}>
{row.cells.map((c) => {
return (
<Grid.Cell
key={c.id}
cell={c}
className="text-sm flex items-center px-2 h-full w-full"
/>
);
})}
</Grid.Row>
);
})}
</Grid.RowsCenter>
</Grid.RowsContainer>
</Grid.Viewport>
</Grid.Root>
</div>
<div className="lng-grid" style={{ height: 200 }}>
<Grid.Root grid={lower}>
<Grid.Viewport>
<Grid.Header>
{viewLower.header.layout.map((row, i) => {
return (
<Grid.HeaderRow key={i} headerRowIndex={i}>
{row.map((c) => {
if (c.kind === "group") return null;
return (
<Grid.HeaderCell
key={c.id}
cell={c}
className="flex w-full h-full capitalize px-2 items-center"
/>
);
})}
</Grid.HeaderRow>
);
})}
</Grid.Header>
<Grid.RowsContainer>
<Grid.RowsCenter>
{viewLower.rows.center.map((row) => {
if (row.kind === "full-width") return null;
return (
<Grid.Row row={row} key={row.id} accepted={["row"]}>
{row.cells.map((c) => {
return (
<Grid.Cell
key={c.id}
cell={c}
className="text-sm flex items-center px-2 h-full w-full"
/>
);
})}
</Grid.Row>
);
})}
</Grid.RowsCenter>
</Grid.RowsContainer>
</Grid.Viewport>
</Grid.Root>
</div>
</div>
);
}
LyteNyte Grid provides a DropWrap
component for external drop zones. DropWrap
is a simple div
that handles all the necessary drag events and can accept any drag data produced by the grid.
"use client";
import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";
import "@1771technologies/lytenyte-pro/grid.css";
import { DragDotsSmallIcon } from "@1771technologies/lytenyte-pro/icons";
import type { Column } from "@1771technologies/lytenyte-pro/types";
import { bankDataSmall } from "@1771technologies/sample-data/bank-data-smaller";
import { useId } from "react";
type BankData = (typeof bankDataSmall)[number];
const columns: Column<BankData>[] = [
{ id: "education" },
{ id: "marital" },
{ id: "age", type: "number" },
{ id: "job" },
{ id: "balance", type: "number" },
{ id: "default" },
{ id: "housing" },
{ id: "loan" },
{ id: "contact" },
{ id: "day", type: "number" },
{ id: "month" },
{ id: "duration" },
{ id: "campaign" },
{ id: "pdays" },
{ id: "previous" },
{ id: "poutcome" },
{ id: "y" },
];
export default function RowDraggingBetweenGrids() {
const ds = useClientRowDataSource({
data: bankDataSmall,
});
const upper = Grid.useLyteNyte({
gridId: useId(),
rowDataSource: ds,
columns,
columnMarkerEnabled: true,
columnMarker: {
cellRenderer: (p) => {
const drag = p.grid.api.useRowDrag({
getDragData: () => {
return {
siteLocalData: {
row: p.rowIndex,
},
};
},
placeholder: () => {
return (
<div className="flex p-2 rounded bg-black text-white">
Dragging row at index: {p.rowIndex}
</div>
);
},
onDrop: (p) => {
alert(
`Dropped row at ${p.state.siteLocalData?.row ?? ""} ${
p.moveState.topHalf ? "before" : "after"
} row ${p.dropElement.getAttribute("data-ln-rowindex")}`
);
},
});
return (
<span {...drag.dragProps}>
<DragDotsSmallIcon />
</span>
);
},
},
});
const lower = Grid.useLyteNyte({
gridId: useId(),
rowDataSource: ds,
columns,
columnMarkerEnabled: true,
columnMarker: {
cellRenderer: (p) => {
const drag = p.grid.api.useRowDrag({
getDragData: () => {
return {
siteLocalData: {
row: p.rowIndex,
},
};
},
placeholder: () => {
return (
<div className="flex p-2 rounded bg-black text-white">
Dragging row at index: {p.rowIndex}
</div>
);
},
onDrop: (p) => {
alert(
`Dropped row at ${p.state.siteLocalData?.row ?? ""} ${
p.moveState.topHalf ? "before" : "after"
} row ${p.dropElement.getAttribute("data-ln-rowindex")}`
);
},
});
return (
<span {...drag.dragProps}>
<DragDotsSmallIcon />
</span>
);
},
},
});
const viewUpper = upper.view.useValue();
const viewLower = lower.view.useValue();
return (
<div className="flex flex-col gap-8">
<div className="lng-grid" style={{ height: 200 }}>
<Grid.Root grid={upper}>
<Grid.Viewport>
<Grid.Header>
{viewUpper.header.layout.map((row, i) => {
return (
<Grid.HeaderRow key={i} headerRowIndex={i}>
{row.map((c) => {
if (c.kind === "group") return null;
return (
<Grid.HeaderCell
key={c.id}
cell={c}
className="flex w-full h-full capitalize px-2 items-center"
/>
);
})}
</Grid.HeaderRow>
);
})}
</Grid.Header>
<Grid.RowsContainer>
<Grid.RowsCenter>
{viewUpper.rows.center.map((row) => {
if (row.kind === "full-width") return null;
return (
<Grid.Row row={row} key={row.id} accepted={["row"]}>
{row.cells.map((c) => {
return (
<Grid.Cell
key={c.id}
cell={c}
className="text-sm flex items-center px-2 h-full w-full"
/>
);
})}
</Grid.Row>
);
})}
</Grid.RowsCenter>
</Grid.RowsContainer>
</Grid.Viewport>
</Grid.Root>
</div>
<div className="lng-grid" style={{ height: 200 }}>
<Grid.Root grid={lower}>
<Grid.Viewport>
<Grid.Header>
{viewLower.header.layout.map((row, i) => {
return (
<Grid.HeaderRow key={i} headerRowIndex={i}>
{row.map((c) => {
if (c.kind === "group") return null;
return (
<Grid.HeaderCell
key={c.id}
cell={c}
className="flex w-full h-full capitalize px-2 items-center"
/>
);
})}
</Grid.HeaderRow>
);
})}
</Grid.Header>
<Grid.RowsContainer>
<Grid.RowsCenter>
{viewLower.rows.center.map((row) => {
if (row.kind === "full-width") return null;
return (
<Grid.Row row={row} key={row.id} accepted={["row"]}>
{row.cells.map((c) => {
return (
<Grid.Cell
key={c.id}
cell={c}
className="text-sm flex items-center px-2 h-full w-full"
/>
);
})}
</Grid.Row>
);
})}
</Grid.RowsCenter>
</Grid.RowsContainer>
</Grid.Viewport>
</Grid.Root>
</div>
</div>
);
}