Row Dragging
Drag and drop rows within the same LyteNyte Grid, across multiple grids, or into external drop zones.
Single Row Dragging
LyteNyte Grid implements row dragging through hooks. Use the grid.api.useRowDrag hook to enable row
dragging. This hook returns drag props that you attach to a drag handle component. It requires a
getDragData callback, which must return 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 acts as a
drop zone. Set its accepted property to define which drag data it can receive. The demo below shows
a basic single-row drag setup. Focus on the marker column's cellRenderer and the accepted prop on
Grid.Row.
Single Row Dragging
"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/grid-sample-data/bank-data-smaller";import { useId } from "react";import { BalanceCell, DurationCell, NumberCell, tw } from "./components";type BankData = (typeof bankDataSmall)[number];const columns: Column<BankData>[] = [{ id: "job", width: 120 },{ id: "age", type: "number", width: 80, cellRenderer: NumberCell },{ id: "balance", type: "number", cellRenderer: BalanceCell },{ id: "education" },{ id: "marital" },{ id: "default" },{ id: "housing" },{ id: "loan" },{ id: "contact" },{ id: "day", type: "number", cellRenderer: NumberCell },{ id: "month" },{ id: "duration", type: "number", cellRenderer: DurationCell },{ id: "poutcome", name: "P Outcome" },{ id: "y" },];export default function RowDragging() {const ds = useClientRowDataSource({data: bankDataSmall,});const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: { width: 100 },columnMarkerEnabled: true,columnMarker: {cellRenderer: (p) => {const drag = p.grid.api.useRowDrag({placeholder: (_, el) => el.parentElement?.parentElement ?? el,getDragData: () => {return {siteLocalData: {row: p.rowIndex,},};},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.HeaderCellkey={c.id}cell={c}className={tw("flex items-center px-2 text-sm capitalize",c.column.type === "number" && "justify-end",)}/>);})}</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.Cellkey={c.id}cell={c}className={tw("flex items-center px-2 text-sm",c.column.type === "number" && "justify-end tabular-nums",)}/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type { CellRendererParams } from "@1771technologies/lytenyte-pro/types";import type { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";export type BankData = (typeof bankDataSmall)[number];export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}const formatter = new Intl.NumberFormat("en-US", {maximumFractionDigits: 2,minimumFractionDigits: 0,});export function BalanceCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);if (typeof field === "number") {if (field < 0) return `-$${formatter.format(Math.abs(field))}`;return "$" + formatter.format(field);}return `${field ?? "-"}`;}export function DurationCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? `${formatter.format(field)} days` : `${field ?? "-"}`;}export function NumberCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? formatter.format(field) : `${field ?? "-"}`;}
Dragging Multiple Rows
Combine row dragging with row selection to support dragging multiple rows. Instead of returning data
for a single row, use getDragData to include all selected rows from the selection state.
Multiple Row Dragging
"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/grid-sample-data/bank-data-smaller";import { useId } from "react";import { BalanceCell, DurationCell, NumberCell, tw } from "./components";type BankData = (typeof bankDataSmall)[number];const columns: Column<BankData>[] = [{ id: "job", width: 120 },{ id: "age", type: "number", width: 80, cellRenderer: NumberCell },{ id: "balance", type: "number", cellRenderer: BalanceCell },{ id: "education" },{ id: "marital" },{ id: "default" },{ id: "housing" },{ id: "loan" },{ id: "contact" },{ id: "day", type: "number", cellRenderer: NumberCell },{ id: "month" },{ id: "duration", type: "number", cellRenderer: DurationCell },{ id: "poutcome", name: "P Outcome" },{ id: "y" },];export default function RowDraggingMultiple() {const ds = useClientRowDataSource({data: bankDataSmall,});const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: { width: 100 },rowSelectionMode: "multiple",rowSelectionActivator: "single-click",columnMarkerEnabled: true,columnMarker: {cellRenderer: (p) => {const drag = p.grid.api.useRowDrag({placeholder: (_, el) => el.parentElement?.parentElement ?? el,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])],},};},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.HeaderCellkey={c.id}cell={c}className={tw("flex items-center px-2 text-sm capitalize",c.column.type === "number" && "justify-end",)}/>);})}</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.Cellkey={c.id}cell={c}className={tw("flex items-center px-2 text-sm",c.column.type === "number" && "justify-end tabular-nums",)}/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type { CellRendererParams } from "@1771technologies/lytenyte-pro/types";import type { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";export type BankData = (typeof bankDataSmall)[number];export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}const formatter = new Intl.NumberFormat("en-US", {maximumFractionDigits: 2,minimumFractionDigits: 0,});export function BalanceCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);if (typeof field === "number") {if (field < 0) return `-$${formatter.format(Math.abs(field))}`;return "$" + formatter.format(field);}return `${field ?? "-"}`;}export function DurationCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? `${formatter.format(field)} days` : `${field ?? "-"}`;}export function NumberCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? formatter.format(field) : `${field ?? "-"}`;}
Dragging Between Grids
To enable dragging between grids, use a shared accepted value. For example, setting
accepted={["row"]} on both grids allows drag data containing a row key to be dropped onto rows in
either grid.
Grid-to-Grid Dragging
"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/grid-sample-data/bank-data-smaller";import { useId } from "react";import { BalanceCell, DurationCell, NumberCell, tw } from "./components";type BankData = (typeof bankDataSmall)[number];const columns: Column<BankData>[] = [{ id: "job", width: 120 },{ id: "age", type: "number", width: 80, cellRenderer: NumberCell },{ id: "balance", type: "number", cellRenderer: BalanceCell },{ id: "education" },{ id: "marital" },{ id: "default" },{ id: "housing" },{ id: "loan" },{ id: "contact" },{ id: "day", type: "number", cellRenderer: NumberCell },{ id: "month" },{ id: "duration", type: "number", cellRenderer: DurationCell },{ id: "poutcome", name: "P Outcome" },{ id: "y" },];export default function RowDraggingBetweenGrids() {const ds = useClientRowDataSource({data: bankDataSmall,});const upper = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: { width: 100 },columnMarkerEnabled: true,columnMarker: {cellRenderer: (p) => {const drag = p.grid.api.useRowDrag({placeholder: (_, el) => el.parentElement?.parentElement ?? el,getDragData: () => {return {siteLocalData: {row: p.rowIndex,},};},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,columnBase: { width: 100 },columnMarkerEnabled: true,columnMarker: {cellRenderer: (p) => {const drag = p.grid.api.useRowDrag({placeholder: (_, el) => el.parentElement?.parentElement ?? el,getDragData: () => {return {siteLocalData: {row: p.rowIndex,},};},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.HeaderCellkey={c.id}cell={c}className={tw("flex items-center px-2 text-sm capitalize",c.column.type === "number" && "justify-end",)}/>);})}</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.Cellkey={c.id}cell={c}className={tw("flex items-center px-2 text-sm",c.column.type === "number" && "justify-end tabular-nums",)}/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div><div className="lng-grid border-t" 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.HeaderCellkey={c.id}cell={c}className={tw("flex items-center px-2 text-sm capitalize",c.column.type === "number" && "justify-end",)}/>);})}</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.Cellkey={c.id}cell={c}className={tw("flex items-center px-2 text-sm",c.column.type === "number" && "justify-end tabular-nums",)}/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div></div>);}
import type { CellRendererParams } from "@1771technologies/lytenyte-pro/types";import type { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";export type BankData = (typeof bankDataSmall)[number];export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}const formatter = new Intl.NumberFormat("en-US", {maximumFractionDigits: 2,minimumFractionDigits: 0,});export function BalanceCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);if (typeof field === "number") {if (field < 0) return `-$${formatter.format(Math.abs(field))}`;return "$" + formatter.format(field);}return `${field ?? "-"}`;}export function DurationCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? `${formatter.format(field)} days` : `${field ?? "-"}`;}export function NumberCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? formatter.format(field) : `${field ?? "-"}`;}
External Drop Zones
Use the DropWrap component to create external drop zones. DropWrap renders a div that handles
all required drag events and can accept any drag data produced by the grid.
External Drop Zones
"use client";import { DropWrap, Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import { DragDotsSmallIcon, DragIcon } from "@1771technologies/lytenyte-pro/icons";import type { Column } from "@1771technologies/lytenyte-pro/types";import { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";import { useId, useState } from "react";import { BalanceCell, DurationCell, NumberCell, tw } from "./components";type BankData = (typeof bankDataSmall)[number];const columns: Column<BankData>[] = [{ id: "job", width: 120 },{ id: "age", type: "number", width: 80, cellRenderer: NumberCell },{ id: "balance", type: "number", cellRenderer: BalanceCell },{ id: "education" },{ id: "marital" },{ id: "default" },{ id: "housing" },{ id: "loan" },{ id: "contact" },{ id: "day", type: "number", cellRenderer: NumberCell },{ id: "month" },{ id: "duration", type: "number", cellRenderer: DurationCell },{ id: "poutcome", name: "P Outcome" },{ id: "y" },];export default function RowDraggingExternalDropZone() {const ds = useClientRowDataSource({data: bankDataSmall,});const [dropped, setDropped] = useState<BankData[]>([]);const upper = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: { width: 100 },columnMarkerEnabled: true,columnMarker: {cellRenderer: (p) => {const drag = p.grid.api.useRowDrag({placeholder: (_, el) => el.parentElement?.parentElement ?? el,getDragData: () => {return {siteLocalData: {row: p.row.data as BankData,},};},onDrop: (p) => {setDropped((prev) => [...prev, p.state.siteLocalData?.row as BankData]);},});return (<span {...drag.dragProps}><DragDotsSmallIcon /></span>);},},});const grid = upper.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>{grid.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("flex items-center px-2 text-sm capitalize",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{grid.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className={tw("flex items-center px-2 text-sm",c.column.type === "number" && "justify-end tabular-nums",)}/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div><DropWrapaccepted={["row"]}className="data-[ln-can-drop=true]:border-ln-primary-50 overflow-auto border border-dashed"style={{ height: 200 }}>{dropped.length === 0 && (<div className="flex h-full w-full items-center justify-center"><DragIcon className="size-8" /><div className="text-center">Drag a row here</div></div>)}{dropped.map((c, i) => {return (<div className="flex items-center gap-2 text-nowrap px-2 py-1" key={i}><span className="text-ln-gray-100 font-semibold">Row Value:</span>{" "}<span className="font-mono">{JSON.stringify(c)}</span></div>);})}</DropWrap></div>);}
import type { CellRendererParams } from "@1771technologies/lytenyte-pro/types";import type { bankDataSmall } from "@1771technologies/grid-sample-data/bank-data-smaller";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";export type BankData = (typeof bankDataSmall)[number];export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}const formatter = new Intl.NumberFormat("en-US", {maximumFractionDigits: 2,minimumFractionDigits: 0,});export function BalanceCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);if (typeof field === "number") {if (field < 0) return `-$${formatter.format(Math.abs(field))}`;return "$" + formatter.format(field);}return `${field ?? "-"}`;}export function DurationCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? `${formatter.format(field)} days` : `${field ?? "-"}`;}export function NumberCell({ grid, row, column }: CellRendererParams<BankData>) {const field = grid.api.columnField(column, row);return typeof field === "number" ? formatter.format(field) : `${field ?? "-"}`;}
Next Steps
- Row Pinning: Freeze rows at the top or bottom of the viewport.
- Row Full Width: Create rows that span the full width of the viewport.
- Row Selection: Select single or multiple rows.
- Row Sorting: Learn how to sort and order rows.