Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions examples/react/row-dnd/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
"start": "vite"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@faker-js/faker": "^8.3.1",
"@tanstack/react-table": "^8.12.0",
"react": "^18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions examples/react/row-dnd/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ th {
td {
border-right: 1px solid lightgray;
padding: 2px 4px;
background-color: white;
}

td button {
Expand Down
284 changes: 153 additions & 131 deletions examples/react/row-dnd/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC } from 'react'
import React, { CSSProperties } from 'react'
import ReactDOM from 'react-dom/client'

import './index.css'
Expand All @@ -12,177 +12,199 @@ import {
} from '@tanstack/react-table'
import { makeData, Person } from './makeData'

import { DndProvider, useDrag, useDrop } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
// needed for table body level scope DnD setup
import {
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
closestCenter,
type DragEndEvent,
type UniqueIdentifier,
useSensor,
useSensors,
} from '@dnd-kit/core'
import { restrictToVerticalAxis } from '@dnd-kit/modifiers'
import {
arrayMove,
SortableContext,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'

// needed for row & cell level scope DnD setup
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'

// Cell Component
const RowDragHandleCell = ({ rowId }: { rowId: string }) => {
const { attributes, listeners } = useSortable({
id: rowId,
})
return (
// Alternatively, you could set these attributes on the rows themselves
<button {...attributes} {...listeners}>
🟰
</button>
)
}

// Row Component
const DraggableRow = ({ row }: { row: Row<Person> }) => {
const { transform, transition, setNodeRef, isDragging } = useSortable({
id: row.original.userId,
})

const style: CSSProperties = {
transform: CSS.Transform.toString(transform),
transition: transition, //let dnd-kit do its thing
opacity: isDragging ? 0.8 : 1,
zIndex: isDragging ? 1 : 0,
position: 'relative',
}
return (
// connect row ref to dnd-kit, apply important styles
<tr ref={setNodeRef} style={style}>
{row.getVisibleCells().map(cell => (
<td key={cell.id} style={{ width: cell.column.getSize() }}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
)
}

const defaultColumns: ColumnDef<Person>[] = [
{
header: 'Name',
footer: props => props.column.id,
columns: [
// Table Component
function App() {
const columns = React.useMemo<ColumnDef<Person>[]>(
() => [
// Create a dedicated drag handle column. Alternatively, you could just set up dnd events on the rows themselves.
{
id: 'drag-handle',
header: 'Move',
cell: ({ row }) => <RowDragHandleCell rowId={row.id} />,
size: 60,
},
{
accessorKey: 'firstName',
cell: info => info.getValue(),
footer: props => props.column.id,
},
{
accessorFn: row => row.lastName,
id: 'lastName',
cell: info => info.getValue(),
header: () => <span>Last Name</span>,
footer: props => props.column.id,
},
],
},
{
header: 'Info',
footer: props => props.column.id,
columns: [
{
accessorKey: 'age',
header: () => 'Age',
footer: props => props.column.id,
},
{
header: 'More Info',
columns: [
{
accessorKey: 'visits',
header: () => <span>Visits</span>,
footer: props => props.column.id,
},
{
accessorKey: 'status',
header: 'Status',
footer: props => props.column.id,
},
{
accessorKey: 'progress',
header: 'Profile Progress',
footer: props => props.column.id,
},
],
accessorKey: 'visits',
header: () => <span>Visits</span>,
},
{
accessorKey: 'status',
header: 'Status',
},
{
accessorKey: 'progress',
header: 'Profile Progress',
},
],
},
]

const DraggableRow: FC<{
row: Row<Person>
reorderRow: (draggedRowIndex: number, targetRowIndex: number) => void
}> = ({ row, reorderRow }) => {
const [, dropRef] = useDrop({
accept: 'row',
drop: (draggedRow: Row<Person>) => reorderRow(draggedRow.index, row.index),
})

const [{ isDragging }, dragRef, previewRef] = useDrag({
collect: monitor => ({
isDragging: monitor.isDragging(),
}),
item: () => row,
type: 'row',
})

return (
<tr
ref={previewRef} //previewRef could go here
style={{ opacity: isDragging ? 0.5 : 1 }}
>
<td ref={dropRef}>
<button ref={dragRef}>🟰</button>
</td>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
[]
)
}

function App() {
const [columns] = React.useState(() => [...defaultColumns])
const [data, setData] = React.useState(() => makeData(20))

const reorderRow = (draggedRowIndex: number, targetRowIndex: number) => {
data.splice(targetRowIndex, 0, data.splice(draggedRowIndex, 1)[0] as Person)
setData([...data])
}
const dataIds = React.useMemo<UniqueIdentifier[]>(
() => data?.map(({ userId }) => userId),
[data]
)

const rerender = () => setData(() => makeData(20))

const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getRowId: row => row.userId, //good to have guaranteed unique row ids/keys for rendering
getRowId: row => row.userId, //required because row indexes will change
debugTable: true,
debugHeaders: true,
debugColumns: true,
})

// reorder columns after drag & drop
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event
if (active && over && active.id !== over.id) {
setData(data => {
const oldIndex = dataIds.indexOf(active.id)
const newIndex = dataIds.indexOf(over.id)
return arrayMove(data, oldIndex, newIndex) //this is just a splice util
})
}
}

const sensors = useSensors(
useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}),
useSensor(KeyboardSensor, {})
)

return (
<div className="p-2">
<div className="h-4" />
<div className="flex flex-wrap gap-2">
<button onClick={() => rerender()} className="border p-1">
Regenerate
</button>
</div>
<div className="h-4" />
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
<th />
{headerGroup.headers.map(header => (
<th key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<DraggableRow key={row.id} row={row} reorderRow={reorderRow} />
))}
</tbody>
<tfoot>
{table.getFooterGroups().map(footerGroup => (
<tr key={footerGroup.id}>
{footerGroup.headers.map(header => (
<th key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.footer,
header.getContext()
)}
</th>
// NOTE: This provider creates div elements, so don't nest inside of <table> elements
<DndContext
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
onDragEnd={handleDragEnd}
sensors={sensors}
>
<div className="p-2">
<div className="h-4" />
<div className="flex flex-wrap gap-2">
<button onClick={() => rerender()} className="border p-1">
Regenerate
</button>
</div>
<div className="h-4" />
<table>
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
<SortableContext
items={dataIds}
strategy={verticalListSortingStrategy}
>
{table.getRowModel().rows.map(row => (
<DraggableRow key={row.id} row={row} />
))}
</tr>
))}
</tfoot>
</table>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
</SortableContext>
</tbody>
</table>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
</DndContext>
)
}

const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')

ReactDOM.createRoot(rootElement).render(
// <React.StrictMode> //disabled for react-dnd preview bug for now
<DndProvider backend={HTML5Backend}>
<React.StrictMode>
<App />
</DndProvider>
// </React.StrictMode>
</React.StrictMode>
)
4 changes: 2 additions & 2 deletions examples/react/row-dnd/src/makeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const range = (len: number) => {

const newPerson = (): Person => {
return {
userId: faker.datatype.uuid(),
userId: faker.string.uuid(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
age: faker.number.int(40),
Expand All @@ -38,7 +38,7 @@ const newPerson = (): Person => {
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((d): Person => {
return range(len).map((_d): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
Expand Down
Loading