This library is still in development and is available only for internal usage at VTEX.
Skip to main content

DataGrid

Version: 0.131.0

DataGrid

DataGrid is designed to render tabular data consistently for any kind of data type. If you're coming from VTEX Styleguide, you can see it as the next-gen of the Table v2 (and is strongly inspired by it).

State

DataGrid is pretty much stateless. The state hook useDataGridState contains all business logic needed for the component.

function Example() {
/**
* The hook returns the DataGrid state
*/
const state = useDataGridState({
/**
* Columns shape, read more about it on the rendering section
*/
columns: [
{
id: 'productName',
header: 'Product name',
},
{
id: 'inStock',
header: 'In Stock',
},
{
id: 'price',
header: 'Price',
},
{
id: 'skus',
header: 'SKUs',
},
],
/**
* List of items to render
*/
items: [
{
id: 1,
productName: 'Orange',
inStock: 380,
skus: 0,
price: 120,
},
],
})
/**
* You must use the `state` prop so that your DataGrid comes to life
* This is the only prop that is required
*/
return <DataGrid state={state} />
}

useDataGridState

NameTypeDescriptionRequiredDefault
columnsColumn<T>[]Table column spec-
viewDataViewStateRelated DataView state🚫-
contextResolverContextResolver context🚫-
resolversRecord<string, Resolver<T>>Table field resolvers🚫Table's default resolvers
itemsT[]Table items🚫[]
lengthnumberExpected items length, this will also control the number of skeleton items🚫5
sortUseTableSortParams<T>useTableSort hook params🚫-
getRowKey(item: T) => stringKey extractor🚫Table's default key extractor
densityTableDensityInital table row height🚫regular
onRowClick(item: T) => voidAction to dispatch on a row click🚫-

It returns an object with the following types

interface DataGridState<T> {
/**
* Collection rendered while loading
*/
skeletonCollection: T[]
/**
* Resolves the cell content
*/
resolveCell: (args: ResolverCallee<ResolveCellArgs<T>>) => ReactNode
/**
* Resolvers the header content
*/
resolveHeader: (
args: ResolverCallee<ResolveHeaderArgs<T>>
) => ResolveHeaderReturn
/**
* Items to render
*/
data: T[]
/**
* Grid columns
*/
columns: Array<Column<T>>
/**
* Providers from the resolvers
*/
Providers: (props: PropsWithChildren<unknown>) => JSX.Element
/**
* Current sorting state
*/
sortState: UseSortReturn
/**
* Key extractor
*/
getRowKey: (item: T) => string | unknown
/**
* Current grid density
*/
density: DataGridDensity
/**
* Set the current grid density
*/
setDensity: React.Dispatch<DataGridDensity>
/**
* Action to take on click a row
*/
onRowClick?: (item: T) => void
}

/**
* Caller of a resolver
*/
type ResolverCallee<T> = Omit<T, 'resolvers' | 'context' | 'sortState'>

Rendering

The main objective of DataGrid is to provide a flexible render to support any kind of data type.

AttributeTypeDescriptionRequired
idstringString that defines the property name that the column represents.
header((column: Column<T>) => ReactNode), or stringControls the title which appears on the table Header.
It can receive either a string or an element.
🚫
accessor((item: T) => ReactNode), or stringDefines how to access a property🚫
resolverRResolvers api
Will select the plain resolver by default
🚫
widthnumberDefines a fixed width for the specific column.
Receives either a string or number.
By default, the column's width is defined to fit the available space without breaking the content.
🚫
sortable(a: T, b: T) => numberDefines if that column is sortable or not, passing true to this prop won't sort items by itself, the sorting will still need to be handled using the sort prop inside the StatelessTable sort prop. Check Sorting🚫
comparebooleanThe function provided to handle the sorting of this column of the table, if this function is provided the table items will be sorted based on this function result. Check Sorting🚫

Accessor

Some properties may be nested within objects and arrays. The accessor properties provide an easy way to access those.

const items = [
{
id: 1,
product: {
name: 'Orange',
type: 'Fruit',
},
qty: {
sold: 100,
total: 320,
},
skus: {
value: [0, 10, 20],
},
price: [120, 'usd'],
},
]

function Example() {
const state = useDataGridState({
columns: [
{
id: 'product.name',
header: 'Name',
accessor: 'product.name',
},
{
id: 'product.type',
header: 'Type',
accessor: 'product.type',
},
{
id: 'qty',
header: 'In Stock',
accessor: (item) => {
const {
qty: { total, sold },
} = item
return total - sold
},
},
{
id: 'price',
header: 'Price',
accessor: 'price.0',
},
{
id: 'skus',
header: 'SKUs',
accessor: 'skus.value.2',
},
],
items,
})

return <DataGrid state={state} />
}

render(<Example />)

Resolvers

Resolvers are rendering functions that target a specific data type. The main usage is to render the same data types consistently along with admin applications.

Key concepts

Render function

All resolvers accept a render function, that returns a component. It controls the data rendering, which may be treated by the resolver or not.

{
type: 'resolver name',
/**
* You have 3 render props here:
* { item, data, context }
*/
render: function Render({ item, data, context }) {
return <></>
}
}
NameTypeDescription
itemTthe item displayed for the row
dataunknownextracted column data from the item, you need to cast it before use
context{ loading: boolean, density: DataGridDensity }relevant global information about the table current state
Root Resolver

This is the parent of all other resolvers. It does not treat the data at all - even the loading state is completely up to you. Use it if you want complete control over what's being rendered on the cell, and don't mind the complexity that it brings.

function Example() {
const state = useDataGridState({
columns: [
{
id: 'id',
header: 'Id',
},
/**
* The great thing about the root resolver is that you can infer new columns from
* multiple properties of the item.
*
* description is being created from productName and category
*/
{
id: 'description',
header: 'Description',
resolver: {
type: 'root',
/**
* { data } here would be null, because the is no such prop in the item
*/
render: function Description({ item, context }) {
/**
* You should declare the render while loading
* this is only required by the root resolver
* the other ones, take care of this for you
*/
if (context.status === 'loading') {
return <Skeleton csx={{ height: 24 }} />
}

return (
<Stack>
<Text variant="highlight">{item.productName}</Text>
<Text>{item.category}</Text>
</Stack>
)
},
},
},
{
id: 'inStock',
header: 'In Stock',
},
],
items: [
{
id: 1,
productName: 'Orange',
category: 'fruit',
inStock: 380,
},
{
id: 2,
productName: 'Lemon',
category: 'fruit',
inStock: 380,
},
],
})

return <DataGrid state={state} />
}

Resolver Options

Plain

The plain resolver is the default for all columns. It means that if you don't select a resolver, this is what you're rendering. It should be mainly used to render raw data like strings or numbers that don't need treatment.

const items = [
{
id: 1,
productName: 'Orange',
inStock: 380,
skus: 0,
price: 120,
},
{
id: 2,
productName: 'Lemon',
inStock: 380,
skus: 26,
price: 120,
},
{
id: 3,
productName: 'Tomato',
inStock: 380,
skus: 26,
price: 120,
},
]

function Example() {
const state = useDataGridState({
columns: [
{
id: 'productName',
header: 'Product name',
},
{
id: 'inStock',
header: 'In Stock',
},
{
id: 'price',
header: 'Price',
},
{
id: 'skus',
header: 'SKUs',
resolver: {
type: 'plain',
/**
* this is how to use the render function
*/
render: function Render({ data }) {
return (
<Text tone={Number(data) > 0 ? 'info' : 'critical'}>{data}</Text>
)
},
},
},
],
items,
})

return <DataGrid state={state} />
}

render(<Example />)
Currency
function Example() {
const currencies = [
{
id: 1,
brl: 120,
usd: 24,
cny: 100,
},
]

const state = useDataGridState({
columns: [
{
id: 'brl',
header: 'Preço',
resolver: {
type: 'currency',
locale: 'pt-BR',
currency: 'BRL',
},
},
{
id: 'usd',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
{
id: 'cny',
header: '价格',
resolver: {
type: 'currency',
locale: 'zh-CN',
currency: 'CNY',
},
},
],
items: currencies,
})

return <DataGrid state={state} />
}
Date
function Example() {
const dates = [
{
id: 1,
pt: '5/7/2020, 13:04',
ar: '5/7/2020, 13:04',
en: '5/7/2020, 13:04',
cn: '5/7/2020, 13:04',
},
]

const state = useDataGridState({
columns: [
{
id: 'pt',
header: 'Data',
resolver: {
type: 'date',
locale: 'pt-BR',
options: {
day: 'numeric',
month: 'long',
year: 'numeric',
},
},
},
{
id: 'ar',
header: 'تاريخ',
resolver: {
type: 'date',
locale: 'ar-AE',
options: {
day: 'numeric',
month: 'long',
year: 'numeric',
},
},
},
{
id: 'en',
header: 'Date',
resolver: {
type: 'date',
locale: 'en-US',
options: {
day: 'numeric',
month: 'long',
year: 'numeric',
},
},
},
{
id: 'cn',
header: '日期',
resolver: {
type: 'date',
locale: 'zh-CN',
options: {
day: 'numeric',
month: 'long',
year: 'numeric',
},
},
},
],
items: dates,
})

return <DataGrid state={state} />
}
Image
PropTypeDescriptionRequiredDefault
displaybooleanif should preview on hover🚫true
sizesmall, regular or largesize of the preview🚫regular
delaynumberdelay of a preview display in ms🚫0
function Example() {
const fruits = [
{
id: 1,
image:
'https://images.unsplash.com/photo-1587735243615-c03f25aaff15?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1600&q=80',
productName: 'Orange',
stock: 26900,
price: 120,
},
{
id: 2,
image:
'https://images.unsplash.com/flagged/photo-1587302164675-820fe61bbd55?ixlib=rb-1.2.1&auto=format&fit=crop&w=1600&q=80',
productName: 'Lemon',
stock: 12905,
price: 120,
},
{
id: 3,
image:
'https://images.unsplash.com/photo-1587486938113-d6d38d424efa?ixlib=rb-1.2.1&auto=format&fit=crop&w=1600&q=80',
productName: 'Tomato',
stock: 199001,
price: 120,
},
]

const state = useDataGridState({
columns: [
{
id: 'image',
header: 'Image',
resolver: {
type: 'image',
preview: {
display: true,
size: 'regular',
delay: 0,
},
},
},
{
id: 'productName',
header: 'Name',
},
{
id: 'stock',
header: 'Stock',
},
{
id: 'price',
header: 'Stock',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
],
items: fruits,
})

return <DataGrid state={state} />
}

Sorting

To use the base sorting configuration, that matches the majority of use cases, you just need to pass the compare function to the columns that you want to sort by. Two params are accepted, representing two items - you must return a boolean that proves their equality.

type Compare = (a: T, b: T) => boolean

The following example allows ordering by name, lastSale and price.

const items = Array(3)
.fill()
.map((_, id) => {
return {
id: `${id}`,
name: faker.commerce.productName(),
lastSale: faker.date.past().toDateString(),
price: faker.commerce.price(),
}
})

function CompareExample() {
const state = useDataGridState({
columns: [
{
id: 'name',
header: 'Product Name',
compare: (a, b) => b.name.localeCompare(a.name),
},
{
id: 'lastSale',
header: 'Last Sale',
compare: (a, b) => {
const aLastSale = new Date(a.lastSale).valueOf()
const bLastSale = new Date(b.lastSale).valueOf()

return bLastSale - aLastSale
},
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
compare: (a, b) => parseInt(b.price, 10) - parseInt(a.price, 10),
},
],
items,
})

return <DataGrid state={state} />
}

render(<CompareExample />)

Configuration

By using the sort property within useDataGridState you can configure the sorting to match specific use cases.

initialValue

Defines the table initial sorting value. { order?: 'ASC' | 'DSC', by?: string }

The order prop is related to the sorting order and by indicates which column is being sorted, this value should be the id of the column.

directions

Defines the sorting order of the table.

It accepts an array with ASC and DSC as possible values. You can pass an array with one or two sorting directions. If you pass an array with only one sorting direction the table will only sort in one direction.

reducer

Receives the reducer that will be used inside of the useReducer that handles the sorting state, it is not required and if not provided the default reducer function will be used. The reducer function is called with the current sort state { order?: SortOrder, by?: string } and the sorting action { type: SortOrder | 'RESET', columnId?: string }.

callback

Receives a function that will be fired when the user clicks the table header cell of a column.

This function is called with an object containing the current sort state, the dispatch of the current useReducer that handles the sorting state, the column id of the column that was clicked, and the current sort directions being used.

Composition

The DataGrid is designed to easily integrate with other components to create the view. You must understand the concept of the DataView Pattern before reading this section.

Anatomy

Normally you will encounter DataGrid as the data-rendering part of the data-view, for example:

DataView
|__ DataViewControls
| |__ Search
| |__ Toolbar
| | |__ Button
| |__ Pagination
|
|__ DataGrid
|__ .Head
| |__ .Cell
|__ .Body
|__ .Row
|__ .Cell
function WithinDataView() {
const view = useDataViewState()
const grid = useDataGridState({
columns: [
/**
* For easier usage, you can define the related view
* within inside of the DataGrid state hook
*/
view,
{
id: 'productName',
header: 'Product name',
},
{
id: 'inStock',
header: 'In Stock',
},
{
id: 'price',
header: 'Price',
},
{
id: 'skus',
header: 'SKUs',
},
],
items: [
{
id: 1,
productName: 'Orange',
inStock: 380,
skus: 0,
price: 120,
},
],
})

return (
<DataView state={view}>
<DataGrid state={grid} />
</DataView>
)
}

Search Form

Example using the Search component to filter the data.

const items = [
{
id: 1,
productName: 'Banana',
inStock: 380,
skus: 0,
price: 80,
},
{
id: 2,
productName: 'Lemon',
inStock: 380,
skus: 26,
price: 500,
},
{
id: 3,
productName: 'Tomato',
inStock: 380,
skus: 25,
price: 100,
},
]

function WithSearch() {
const view = useDataViewState()
const search = useSearchState()

const searchedItems = React.useMemo(() => {
return items.filter((item) =>
item.productName.toLowerCase().startsWith(
// use the search debounced value to
// filter the collection
search.debouncedValue.toLocaleLowerCase()
)
)
}, [search])

const grid = useDataGridState({
view,
columns: [
{
id: 'productName',
header: 'Product Name',
},
{
id: 'inStock',
header: 'In Stock',
},
{
id: 'skus',
header: 'SKUs',
},
{
id: 'price',
header: 'Price',
},
],
items: searchedItems,
length: 5,
})

return (
<DataView state={view}>
<DataViewControls>
<Search id="search" placeholder="Search" state={search} />
</DataViewControls>
<DataGrid state={grid} />
</DataView>
)
}

render(<WithSearch />)

Toolbar

Example using the Toolbar component.

function WithToolbar() {
const toolbar = useToolbarState()
const view = useDataViewState()
const grid = useDataGridState({
columns: [
view,
{
id: 'productName',
header: 'Product name',
},
{
id: 'inStock',
header: 'In Stock',
},
{
id: 'price',
header: 'Price',
},
{
id: 'skus',
header: 'SKUs',
},
],
items: [
{
id: 1,
productName: 'Orange',
inStock: 380,
skus: 0,
price: 120,
},
],
})

return (
<DataView state={view}>
<DataViewControls>
<Toolbar state={toolbar}>
<ToolbarButton
size="small"
variant="text"
icon={<IconArrowLineDown />}
>
Import
</ToolbarButton>
<ToolbarButton size="small" variant="text" icon={<IconArrowLineUp />}>
Export
</ToolbarButton>
</Toolbar>
</DataViewControls>
<DataGrid state={grid} />
</DataView>
)
}

Pagination

Example using the Pagination component.

const NUMBER_OF_ITEMS = 100
const ITEMS_PER_PAGE = 5

const items = Array(NUMBER_OF_ITEMS)
.fill()
.map((_, id) => {
return {
id: `${id}`,
name: faker.commerce.productName(),
lastSale: faker.date.past().toDateString(),
price: faker.commerce.price(),
}
})

function WithPagination() {
const view = useDataViewState()
const pagination = usePaginationState({
pageSize: ITEMS_PER_PAGE,
total: NUMBER_OF_ITEMS,
})
const grid = useDataGridState({
view,
columns: [
{
id: 'name',
header: 'Product Name',
},
{
id: 'lastSale',
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
],
items: items.slice(pagination.range[0] - 1, pagination.range[1]),
length: ITEMS_PER_PAGE,
})

return (
<DataView state={view}>
<DataViewControls>
<FlexSpacer />
<Pagination
state={pagination}
preposition="of"
subject="results"
prevLabel="Previous"
nextLabel="Next"
/>
</DataViewControls>
<DataGrid state={grid} />
</DataView>
)
}

render(<WithPagination />)

Reacting to DataView status

By dispatch the setStatus function of the DataView, the DataGrid reacts to it.

const items = Array(3)
.fill()
.map((_, id) => {
return {
id: `${id}`,
name: faker.commerce.productName(),
lastSale: faker.date.past().toDateString(),
price: faker.commerce.price(),
}
})

function StatusExample() {
const view = useDataViewState()
const grid = useDataGridState({
view,
items,
columns: [
{
id: 'name',
header: 'Product Name',
},
{
id: 'lastSale',
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
],
length: 3,
})

return (
<DataView state={view}>
<DataViewControls>
<Button
onClick={() =>
view.setStatus({
type: 'ready',
})
}
>
Ready
</Button>
<Button
onClick={() =>
view.setStatus({
type: 'loading',
})
}
>
Loading
</Button>
<Button
onClick={() =>
view.setStatus({
type: 'error',
message: 'Something went wrong',
})
}
>
Error
</Button>
<Button
onClick={() =>
view.setStatus({
type: 'not-found',
message: 'Your product was not found',
})
}
>
Not Found
</Button>
<Button
onClick={() =>
view.setStatus({
type: 'empty',
message: 'You need to create something',
})
}
>
Empty
</Button>
</DataViewControls>
<DataGrid state={grid} />
</DataView>
)
}

render(<StatusExample />)

Examples

This section presents a series of examples that may be useful.

Filters

Displaying filters and filtering data

const FilterMultiple = experimental_FilterMultiple
const useFilterMultipleState = experimental_useFilterMultipleState
const FilterGroup = experimental_FilterGroup
const useFilterGroupState = experimental_useFilterGroupState
const Filter = experimental_Filter
const useFilterState = experimental_useFilterState

const ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
const items = ids.map((id) => {
return {
id: `${id}`,
name: faker.commerce.productName(),
lastSale: faker.date.past().toDateString(),
price: faker.commerce.price(),
brand: faker.random.arrayElement(['mistery_id', 'cool_id']),
}
})

const columns = createColumns([
{
id: 'name',
header: 'Product Name',
},
{
id: 'brand',
header: 'Brand ID',
},
{
id: 'lastSale',
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
])

function FilterControls() {
const [data, setData] = useState(items)
const view = useDataViewState()

const grid = useDataGridState({
view,
columns,
items: data,
})

const [quality, setQuality] = useState()
const [brand, setBrand] = useState()

const brandFilterState = useFilterMultipleState({
items: [
{ label: 'Mistery brand', id: 'mistery_id' },
{ label: 'Cool brand', id: 'cool_id' },
],
onChange: ({ selected }) => {
setBrand(selected)
},
label: 'Brand',
})

const qualityFilterState = useFilterState({
items: [
{ label: 'Normal', id: 'norm' },
{ label: 'Premium', id: 'prem' },
],
onChange: ({ selected }) => {
setQuality(selected)
},
label: 'Quality',
})

const filterGroupState = useFilterGroupState({
filterStates: [qualityFilterState, brandFilterState],
})

useEffect(() => {
const filtered = items.filter((item) => {
if (quality === 'norm' && Number(item.price) > 510) {
return false
}

if (quality === 'prem' && Number(item.price) <= 510) {
return false
}

if (brand && brand.length) {
if (!brand.includes('cool_id') && item.brand === 'cool_id') {
return false
}

if (!brand.includes('mistery_id') && item.brand === 'mistery_id') {
return false
}
}

return true
})

setData(filtered)
}, [quality, brand])

return (
<DataView csx={{ width: 500 }} state={view}>
<DataViewControls>
<FilterGroup state={filterGroupState}>
<FilterMultiple state={brandFilterState} />
<Filter state={qualityFilterState} />
</FilterGroup>
</DataViewControls>
<DataGrid state={grid} />
</DataView>
)
}

render(<FilterControls />)

Topbar

Mixing the concepts of Search, Toolbar and Pagination

const NUMBER_OF_ITEMS = 100
const ITEMS_PER_PAGE = 5

const items = Array(NUMBER_OF_ITEMS)
.fill()
.map((_, id) => {
return {
id: `${id}`,
name: faker.commerce.productName(),
lastSale: faker.date.past().toDateString(),
price: faker.commerce.price(),
}
})

function WithFullTopbar() {
const toolbar = useToolbarState()
const view = useDataViewState()
const search = useSearchState()
const pagination = usePaginationState({
pageSize: ITEMS_PER_PAGE,
total: NUMBER_OF_ITEMS,
})

const paginatedItems = React.useMemo(() => {
pagination.paginate({ type: 'reset' })
return items.filter((item) =>
item.name.toLowerCase().startsWith(
// use the search debounced value to
// filter the collection
search.debouncedValue.toLocaleLowerCase()
)
)
}, [search.debouncedValue])

const grid = useDataGridState({
view,
columns: [
{
id: 'name',
header: 'Product Name',
},
{
id: 'lastSale',
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
],
items: [...paginatedItems].slice(
pagination.range[0] - 1,
pagination.range[1]
),
length: ITEMS_PER_PAGE,
})

return (
<DataView state={view}>
<DataViewControls>
<Search id="search" placeholder="Search" state={search} />
<Toolbar state={toolbar} aria-label="Toolbar">
<ToolbarButton
size="small"
variant="text"
icon={<IconArrowLineDown />}
>
Import
</ToolbarButton>
<ToolbarButton size="small" variant="text" icon={<IconArrowLineUp />}>
Export
</ToolbarButton>
</Toolbar>
<FlexSpacer />
<Pagination
state={pagination}
preposition="of"
subject="results"
prevLabel="Previous"
nextLabel="Next"
/>
</DataViewControls>
<DataGrid state={grid} />
</DataView>
)
}

render(<WithFullTopbar />)

Data fetching

Example with a simulated data fetching

/**
* Function to simulate a request
* You can configure the delay and numberOfItems here
*/
function request(delay = 3000, numberOfItems = 3) {
return new Promise(function (resolve) {
setTimeout(
resolve,
delay,
Array(numberOfItems)
.fill()
.map((_, id) => {
return {
id: `${id}`,
name: faker.commerce.productName(),
lastSale: faker.date.past().toDateString(),
price: faker.commerce.price(),
}
})
)
})
}

function DataFetchExample() {
const [items, setItems] = React.useState([])
/**
* This is just for the example purposes so, nevermind
*/
const [update, setUpdate] = React.useState(false)

const view = useDataViewState()
const grid = useDataGridState({
view,
columns: [
{
id: 'name',
header: 'Product Name',
},
{
id: 'lastSale',
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
],
length: 3,
items,
})

React.useEffect(() => {
view.setStatus({
type: 'loading',
})
request().then((d) => {
setItems(d)
view.setStatus({
type: 'ready',
})
})
}, [update])

return (
<DataView state={view}>
<DataViewControls>
<Button
onClick={() => {
setUpdate((u) => !u)
}}
>
Simulate data fetching
</Button>
</DataViewControls>
<DataGrid state={grid} />
</DataView>
)
}

render(<DataFetchExample />)

Drag and Drop

Simple and accessible drag and drop reordering using Atlassian's react-beautiful-dnd. The idea here is to use the rendering complexity to access the DataGrid's table internals.

const fakeData = Array(5)
.fill()
.map((_, id) => {
return {
id: `${id}`,
name: faker.commerce.productName(),
lastSale: faker.date.past().toDateString(),
price: faker.commerce.price(),
}
})

// simple reordering function
function reorder(list, startIndex, endIndex) {
const result = Array.from(list)
const [removed] = result.splice(startIndex, 1)

result.splice(endIndex, 0, removed)

return result
}

function Example() {
// we need to keep our items within a state, since they're reordered
const [items, setItems] = React.useState(fakeData)
const datagrid = useDataGridState({
columns: [
// here we create a new column that does not exist on the collection
// this is one of the greatest things about the root resolver
// you can deal with a prop that does not exist without parsing the collection
{
id: 'draggable',
header: '',
width: 36,
resolver: {
type: 'root',
render: function RenderIcon() {
return <IconDotsSixVertical />
},
},
},
// To drag-n-drop look good, each column must have a fixed width
// there is other approaches on this, see:
// https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/patterns/tables.md
{
id: 'name',
width: 200,
header: 'Product Name',
},
{
id: 'lastSale',
width: 200,
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
width: 100,
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
],
items,
})

const onDragEnd = (result) => {
if (!result.destination) {
return
}

const orderedItems = reorder(
items,
result.source.index,
result.destination.index
)

setItems(orderedItems)
}

return (
<DataGrid state={datagrid} csx={{ width: 'unset' }}>
<DataGrid.Head />
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(droppableProvided) => (
<DataGrid.Body ref={droppableProvided.innerRef}>
{(renderRow) => (
<React.Fragment>
{renderRow(({ key, item, index }) => (
<Draggable draggableId={key} index={index}>
{(draggableProvided, draggableSnapshot) => (
<DataGrid.Body.Row
id={key}
item={item}
ref={draggableProvided.innerRef}
{...draggableProvided.draggableProps}
{...draggableProvided.dragHandleProps}
csx={{
...draggableProvided.draggableProps.style,
boxShadow: draggableSnapshot.isDragging
? 'menu'
: 'none',
}}
/>
)}
</Draggable>
))}
{droppableProvided.placeholder}
</React.Fragment>
)}
</DataGrid.Body>
)}
</Droppable>
</DragDropContext>
</DataGrid>
)
}

render(<Example />)