Table
What is this?
Table is designed to render tabular data consistently for any kind of data type.
Composition
The Table
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 Table as the data-rendering part of the data-view, for example:
DataView
|__ DataViewHeader
| |__ Search
| |__ Toolbar
| | |__ Button
| |__ Pagination
|
|__ Table
|__ .Head
| |__ .Cell
|__ .Body
|__ .Row
|__ .Cell
function WithinDataView() {
const view = useDataViewState()
const grid = useTableState({
/**
* For easier usage, you can define the related view
* within inside of the Table state hook
*/
view,
columns: [
{
id: 'name',
header: 'Name',
resolver: {
type: 'text',
columnType: 'name',
mapText: (item) => item.name,
},
},
{
id: 'inStock',
header: 'In Stock',
},
{
id: 'skus',
header: 'SKUs',
},
{
id: 'price',
header: 'Price',
},
{
id: 'status',
header: 'Status',
resolver: {
type: 'root',
render: ({ item }) => {
return <Tag label={item.status} size="normal" />
},
},
},
{
header: 'Actions',
resolver: {
type: 'menu',
actions: [
{
label: 'Add to cart',
},
{
label: 'Buy',
},
],
},
},
],
items: [
{
id: 1,
name: 'Orange',
inStock: 380,
skus: 0,
status: 'Good',
price: 80,
},
],
onRowClick: (item) => alert(`Item: ${item.name}`),
})
return (
<DataView state={view}>
<Table state={grid} />
</DataView>
)
}
const items = [
{
id: 1,
name: 'Orange',
inStock: 380,
skus: 0,
status: 'Good',
price: 80,
},
{
id: 2,
name: 'Lemon',
inStock: 380,
skus: 26,
status: 'Good',
price: 500,
},
{
id: 3,
name: 'Tomato',
inStock: 380,
skus: 25,
status: 'Good',
price: 100,
},
]
function WithSearch() {
const view = useDataViewState()
const search = useSearchState()
const searchedItems = React.useMemo(() => {
return items.filter((item) =>
item.name.toLowerCase().startsWith(
// use the search debounced value to
// filter the collection
search.debouncedValue.toLocaleLowerCase()
)
)
}, [search])
const grid = useTableState({
view,
columns: [
{
id: 'name',
header: 'Name',
resolver: {
type: 'text',
columnType: 'name',
mapText: (item) => item.name,
},
},
{
id: 'inStock',
header: 'In Stock',
},
{
id: 'skus',
header: 'SKUs',
},
{
id: 'price',
header: 'Price',
},
{
id: 'status',
header: 'Status',
resolver: {
type: 'root',
render: ({ item }) => {
return <Tag label={item.status} size="normal" />
},
},
},
{
header: 'Actions',
resolver: {
type: 'menu',
actions: [
{
label: 'Add to cart',
},
{
label: 'Buy',
},
],
},
},
],
items: searchedItems,
length: 5,
onRowClick: (item) => alert(`Item: ${item.name}`),
})
return (
<DataView state={view}>
<DataViewHeader>
<Search id="search" placeholder="Search" state={search} />
</DataViewHeader>
<Table state={grid} />
</DataView>
)
}
render(<WithSearch />)
function WithToolbar() {
const toolbar = useToolbarState()
const view = useDataViewState()
const grid = useTableState({
columns: [
{
id: 'name',
header: 'Name',
resolver: {
type: 'text',
columnType: 'name',
mapText: (item) => item.name,
},
},
{
id: 'inStock',
header: 'In Stock',
},
{
id: 'skus',
header: 'SKUs',
},
{
id: 'price',
header: 'Price',
},
{
id: 'status',
header: 'Status',
resolver: {
type: 'root',
render: ({ item }) => {
return <Tag label={item.status} size="normal" />
},
},
},
{
header: 'Actions',
resolver: {
type: 'menu',
actions: [
{
label: 'Add to cart',
},
{
label: 'Buy',
},
],
},
},
],
items: [
{
id: 1,
name: 'Orange',
inStock: 380,
skus: 0,
status: 'Good',
price: 80,
},
],
onRowClick: (item) => alert(`Item: ${item.name}`),
})
return (
<DataView state={view}>
<DataViewHeader>
<Toolbar state={toolbar}>
<ToolbarButton
size="small"
variant="text"
icon={<IconArrowLineDown />}
>
Import
</ToolbarButton>
<ToolbarButton size="small" variant="text" icon={<IconArrowLineUp />}>
Export
</ToolbarButton>
</Toolbar>
</DataViewHeader>
<Table 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 = useTableState({
view,
columns: [
{
id: 'name',
header: 'Name',
resolver: {
type: 'text',
columnType: 'name',
mapText: (item) => item.name,
},
},
{
id: 'lastSale',
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
{
id: 'status',
header: 'Status',
resolver: {
type: 'root',
render: ({ item }) => {
return <Tag label="Good" size="normal" />
},
},
},
{
header: 'Actions',
resolver: {
type: 'menu',
actions: [
{
label: 'Add to cart',
},
{
label: 'Buy',
},
],
},
},
],
items: items.slice(pagination.range[0] - 1, pagination.range[1]),
length: ITEMS_PER_PAGE,
onRowClick: (item) => alert(`Item: ${item.name}`),
})
return (
<DataView state={view}>
<DataViewHeader>
<FlexSpacer />
<Pagination state={pagination} />
</DataViewHeader>
<Table state={grid} />
</DataView>
)
}
render(<WithPagination />)
Reacting to DataView status
By dispatch the setStatus
function of the DataView
, the Table
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 = useTableState({
view,
items,
columns: [
{
id: 'name',
header: 'Name',
resolver: {
type: 'text',
columnType: 'name',
mapText: (item) => item.name,
},
},
{
id: 'lastSale',
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
{
id: 'status',
header: 'Status',
resolver: {
type: 'root',
render: ({ item, context }) => {
if (context.status === 'loading') {
return <Skeleton csx={{ height: 24 }} />
}
return <Tag label="Good" size="normal" />
},
},
},
{
header: 'Actions',
resolver: {
type: 'menu',
actions: [
{
label: 'Add to cart',
},
{
label: 'Buy',
},
],
},
},
],
length: 3,
onRowClick: (item) => alert(`Item: ${item.name}`),
})
return (
<DataView state={view}>
<DataViewHeader>
<Button
onClick={() =>
view.setStatus({
type: 'ready',
})
}
>
Ready
</Button>
<Button
onClick={() =>
view.setStatus({
type: 'loading',
})
}
>
Loading
</Button>
<Button
onClick={() =>
view.setStatus({
type: 'error',
})
}
>
Error
</Button>
<Button
onClick={() =>
view.setStatus({
type: 'not-found',
})
}
>
Not Found
</Button>
<Button
onClick={() =>
view.setStatus({
type: 'empty',
})
}
>
Empty
</Button>
</DataViewHeader>
<Table 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: 'Name',
resolver: {
type: 'text',
columnType: 'name',
mapText: (item) => item.name,
},
},
{
id: 'brand',
header: 'Brand ID',
},
{
id: 'lastSale',
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
{
id: 'status',
header: 'Status',
resolver: {
type: 'root',
render: ({ item }) => {
return <Tag label="Good" size="normal" />
},
},
},
{
header: 'Actions',
resolver: {
type: 'menu',
actions: [
{
label: 'Add to cart
},
{
label: 'Buy
},
],
},
},
])
function FilterControls() {
const [data, setData] = useState(items)
const view = useDataViewState()
const grid = useTableState({
view,
columns,
items: data,
onRowClick: (item) => alert(`Item: ${item.name}`),
})
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 state={view}>
<DataViewHeader>
<FilterGroup state={filterGroupState}>
<FilterMultiple state={brandFilterState} />
<Filter state={qualityFilterState} />
</FilterGroup>
</DataViewHeader>
<Table 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 = useTableState({
view,
columns: [
{
id: 'name',
header: 'Name',
resolver: {
type: 'text',
columnType: 'name',
mapText: (item) => item.name,
},
},
{
id: 'lastSale',
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
{
id: 'status',
header: 'Status',
resolver: {
type: 'root',
render: ({ item }) => {
return <Tag label="Good" size="normal" />
},
},
},
{
header: 'Actions',
resolver: {
type: 'menu',
actions: [
{
label: 'Add to cart',
},
{
label: 'Buy',
},
],
},
},
],
items: [...paginatedItems].slice(
pagination.range[0] - 1,
pagination.range[1]
),
length: ITEMS_PER_PAGE,
onRowClick: (item) => alert(`Item: ${item.name}`),
})
return (
<DataView state={view}>
<DataViewHeader>
<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"
/>
</DataViewHeader>
<Table 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 = useTableState({
view,
columns: [
{
id: 'name',
header: 'Name',
resolver: {
type: 'text',
columnType: 'name',
mapText: (item) => item.name,
},
},
{
id: 'lastSale',
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
{
id: 'status',
header: 'Status',
resolver: {
type: 'root',
render: ({ item, context }) => {
if (context.status === 'loading') {
return <Skeleton csx={{ height: 24 }} />
}
return <Tag label="Good" size="normal" />
},
},
},
{
header: 'Actions',
resolver: {
type: 'menu',
actions: [
{
label: 'Add to cart',
},
{
label: 'Buy',
},
],
},
},
],
length: 3,
items,
onRowClick: (item) => alert(`Item: ${item.name}`),
})
React.useEffect(() => {
view.setStatus({
type: 'loading',
})
request().then((d) => {
setItems(d)
view.setStatus({
type: 'ready',
})
})
}, [update])
return (
<DataView state={view}>
<DataViewHeader>
<Button
onClick={() => {
setUpdate((u) => !u)
}}
>
Simulate data fetching
</Button>
</DataViewHeader>
<Table state={grid} />
</DataView>
)
}
render(<DataFetchExample />)
Selectable row
Example with a simulated data fetching
function DataFetchExample() {
const grid = useTableState({
columns: [
{
id: 'id',
resolver: {
type: 'selection',
mapId: (item) => item.id,
},
},
{
id: 'name',
header: 'Name',
resolver: {
type: 'text',
columnType: 'name',
mapText: (item) => item.name,
},
},
{
id: 'lastSale',
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
{
id: 'status',
header: 'Status',
resolver: {
type: 'root',
render: ({ item, context }) => {
if (context.status === 'loading') {
return <Skeleton csx={{ height: 24 }} />
}
return <Tag label="Good" size="normal" />
},
},
},
{
header: 'Actions',
resolver: {
type: 'menu',
actions: [
{
label: 'Add to cart',
},
{
label: 'Buy',
},
],
},
},
],
length: 3,
items: React.useMemo(
() =>
Array(3)
.fill()
.map((_, id) => {
return {
id: `${id}`,
name: faker.commerce.productName(),
lastSale: faker.date.past().toDateString(),
price: faker.commerce.price(),
}
}),
[]
),
onRowClick: (item) => alert(`Item: ${item.name}`),
})
const selection = useSelectionTreeState({
items: grid.data,
mapId: (item) => item.id,
})
return (
<SelectionTree state={selection}>
<Table state={grid} />
</SelectionTree>
)
}
render(<DataFetchExample />)
Fixed columns
It is possible to set columns to be fixed when scrolling horizontally. You just need to set the column prop fixed
to true
.
function DataFetchExample() {
const grid = useTableState({
columns: [
{
id: 'id',
fixed: true,
resolver: {
type: 'selection',
mapId: (item) => item.id,
},
},
{
id: 'name',
header: 'Name',
fixed: true,
resolver: {
type: 'text',
columnType: 'name',
mapText: (item) => item.name,
},
},
{
id: 'lastSale',
header: 'Last Sale',
width: 500,
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
{
id: 'status',
header: 'Status',
resolver: {
type: 'root',
render: ({ item, context }) => {
if (context.status === 'loading') {
return <Skeleton csx={{ height: 24 }} />
}
return <Tag label="Good" size="normal" />
},
},
},
{
header: 'Actions',
resolver: {
type: 'menu',
actions: [
{
label: 'Add to cart',
},
{
label: 'Buy',
},
],
},
},
],
length: 3,
items: React.useMemo(
() =>
Array(3)
.fill()
.map((_, id) => {
return {
id: `${id}`,
name: faker.commerce.productName(),
lastSale: faker.date.past().toDateString(),
price: faker.commerce.price(),
}
}),
[]
),
onRowClick: (item) => alert(`Item: ${item.name}`),
})
const selection = useSelectionTreeState({
items: grid.data,
mapId: (item) => item.id,
})
return (
<SelectionTree state={selection}>
<Table state={grid} />
</SelectionTree>
)
}
render(<DataFetchExample />)
Usage
import { Table, useTableState } from '@vtex/admin-ui'
function Example() {
/**
* The hook returns the Table state
*/
const state = useTableState({
/**
* 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 Table comes to life
* This is the only prop that is required
*/
return <Table state={state} />
}
State
The state hook useTableState
contains all business logic needed for the component.
useTableState
Name | Type | Description | Required | Default |
---|---|---|---|---|
columns | Column<T>[] | Table column spec | ✅ | - |
view | DataViewState | Related DataView state | 🚫 | - |
context | ResolverContext | Resolver context | 🚫 | - |
resolvers | Record<string, Resolver<T>> | Table field resolvers | 🚫 | Table's default resolvers |
items | T[] | Table items | 🚫 | [] |
length | number | Expected items length, this will also control the number of skeleton items | 🚫 | 5 |
sort | UseTableSortParams<T> | useTableSort hook params | 🚫 | - |
getRowKey | (item: T) => string | Key extractor | 🚫 | Table's default key extractor |
onRowClick | (item: T) => void | Action to dispatch on a row click | 🚫 | - |
It returns an object with the following types
interface TableState<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
/**
* 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 Table
is to provide a flexible render to support any kind of data type.
Attribute | Type | Description | Required |
---|---|---|---|
id | string | String that defines the property name that the column represents. | ✅ |
header | ((column: Column<T>) => ReactNode), or string | Controls the title which appears on the table Header. It can receive either a string or an element. | 🚫 |
accessor | ((item: T) => ReactNode), or string | Defines how to access a property | 🚫 |
resolver | R | Resolvers api Will select the plain resolver by default | 🚫 |
width | number | Defines 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) => number | Defines 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 | 🚫 |
compare | boolean | The 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 = useTableState({
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,
onRowClick: (item) => alert(`Item: ${item.product.name}`),
})
return <Table 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.
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 <></>
}
}
Name | Type | Description |
---|---|---|
item | T | the item displayed for the row |
data | unknown | extracted column data from the item, you need to cast it before use |
context | { loading: boolean } | relevant global information about the table current state |
Root
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 = useTableState({
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.
*/
{
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
orientation="vertical"
csx={{ justifyContent: 'center', height: 64 }}
>
<Text variant="highlight">
{item.productName} ({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,
},
],
onRowClick: (item) => alert(`Item: ${item.productName}`),
})
return <Table state={state} />
}
Name | Type | Description | Required |
---|---|---|---|
type | root | Root resolver type | ✅ |
render | (props: ResolverRenderProps<null, T>) => ReactNode | Resolver render function | ✅ |
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 = useTableState({
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,
onRowClick: (item) => alert(`Item: ${item.productName}`),
})
return <Table state={state} />
}
render(<Example />)
Name | Type | Description | Required |
---|---|---|---|
type | plain | Plain resolver type | ✅ |
render | (props: ResolverRenderProps<ReactNode, T>) => ReactNode | Resolver render function | 🚫 |
Text
The text resolver should be mainly used to render a text and an optional description just below it. For descriptions that are too long, it is possible to truncante it by setting the overflow
prop.
This resolver must be used on the name
column of the table.
const items = [
{
id: 1,
productName: 'Orange',
description:
'An orange is a fruit of various citrus species in the family Rutaceae (see list of plants known as orange);',
inStock: 380,
skus: 0,
price: 120,
},
{
id: 2,
productName: 'Lemon',
description:
'The lemon (Citrus limon) is a species of small evergreen trees in the flowering plant family Rutaceae, native to Asia, primarily Northeast India (Assam), Northern Myanmar or China',
inStock: 380,
skus: 26,
price: 120,
},
{
id: 3,
productName: 'Tomato',
description:
'The tomato is the edible berry of the plant Solanum lycopersicum,[1][2] commonly known as the tomato plant.',
inStock: 380,
skus: 26,
price: 120,
},
]
function Example() {
const state = useTableState({
columns: [
{
id: 'productName',
header: 'Name',
resolver: {
type: 'text',
columnType: 'name',
overflow: 'ellipsis',
mapText: (item) => item.productName,
mapDescription: (item) => item.description,
},
},
{
id: 'inStock',
header: 'In Stock',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'text',
columnType: 'text',
mapText: (item) => item.price,
mapDescription: () => 'Recently updated',
},
},
{
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,
onRowClick: (item) => alert(`Item: ${item.productName}`),
})
return <Table state={state} />
}
render(<Example />)
Name | Type | Description | Required | Default |
---|---|---|---|---|
type | text | Text resolver type | ✅ | - |
columnType | name or text | Column text type | 🚫 | text |
mapText | (item: T) => ReactNode | The map function which returns the text to be rendered | ✅ | - |
mapDescription | (item: T) => ReactNode | The map Function which returns the description to be rendered | 🚫 | - |
overflow | ellipsis or auto | It specifies how overflowed text should be signaled to the user. | 🚫 | - |
render | (props: ResolverRenderProps<ReactNode, T>) => ReactNode | Resolver render function | 🚫 | - |
Menu
The menu resolver should be used when you want to easily render a Menu component and a set of actions.
const defaultItems = [
{
id: 1,
productName: 'Orange',
description: 'Fruit',
inStock: 380,
skus: 0,
price: 120,
},
{
id: 2,
productName: 'Lemon',
description: 'Fruit',
inStock: 380,
skus: 26,
price: 120,
},
{
id: 3,
productName: 'Tomato',
description: 'Fruit',
inStock: 380,
skus: 26,
price: 120,
},
]
function Example() {
const [items, setItems] = useState(defaultItems)
const state = useTableState({
columns: [
{
id: 'productName',
header: 'Name',
resolver: {
type: 'text',
columnType: 'name',
mapText: (item) => item.productName,
mapDescription: (item) => item.description,
},
},
{
id: 'inStock',
header: 'In Stock',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'text',
columnType: 'text',
mapText: (item) => item.price,
mapDescription: () => 'Recently updated',
},
},
{
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>
)
},
},
},
{
id: 'menu',
resolver: {
type: 'menu',
actions: [
{
label: 'Edit',
icon: <IconPencil />,
onClick: (item) => {
console.log(item)
},
},
{
label: 'Delete',
icon: <IconTrash />,
onClick: (item) => {
setItems(items.filter((i) => i.id !== item.id))
},
},
],
},
},
],
items,
onRowClick: (item) => alert(`Item: ${item.productName}`),
})
return <Table state={state} />
}
render(<Example />)
Name | Type | Description | Required | Default |
---|---|---|---|---|
type | menu | Menu resolver type | ✅ | - |
actions | MenuAction[] | A set of actions to be rendered as MenuItems | ✅ | - |
render | (props: ResolverRenderProps<JSX.Element, T>) => ReactNode | Resolver render function | 🚫 | - |
MenuAction
Name | Type | Description | Required | Default |
---|---|---|---|---|
label | string | MenuItem label | ✅ | - |
onClick | (item: T) => void | MenuItem onClick handler | ✅ | - |
icon | ReactNode | MenuItem icon | 🚫 | - |
disabled | boolean | Whether the MenuItem is disabled or not | 🚫 | false |
critical | boolean | Whether the MenuItem is critical or not | 🚫 | false |
Currency
function Example() {
const currencies = [
{
id: 1,
brl: 120,
usd: 24,
cny: 100,
},
]
const state = useTableState({
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,
onRowClick: (item) => alert(`Item: ${item.id}`),
})
return <Table state={state} />
}
Name | Type | Description | Required |
---|---|---|---|
type | currency | Currency resolver type | ✅ |
locale | string | Currency locale | ✅ |
currency | string | Currency type | ✅ |
render | (props: ResolverRenderProps<string, T>) => ReactNode | Resolver render function | 🚫 |
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 = useTableState({
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,
onRowClick: (item) => alert(`Item: ${item.id}`),
})
return <Table state={state} />
}
Name | Type | Description | Required |
---|---|---|---|
type | date | Date resolver type | ✅ |
locale | string | Date locale | ✅ |
options | Intl.DateTimeFormatOptions | Date options | 🚫 |
render | (props: ResolverRenderProps<string, T>) => ReactNode | Resolver render function | 🚫 |
Image
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 = useTableState({
columns: [
{
id: 'image',
header: 'Image',
resolver: {
type: 'image',
},
},
{
id: 'productName',
header: 'Name',
},
{
id: 'stock',
header: 'Stock',
},
{
id: 'price',
header: 'Stock',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
],
items: fruits,
onRowClick: (item) => alert(`Item: ${item.productName}`),
})
return <Table state={state} />
}
function Example() {
const fruits = [
{
id: 1,
name: 'Color 1',
image:
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRR_gD9Fm-5bttYHoJ-wxD2W8kK2boZsQItYw&usqp=CAU',
price: 100,
},
{
id: 2,
name: 'Color 2',
image:
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRsvxLXJ3jW2hQox375iMAcaScYMpUmXk1dFw&usqp=CAU',
price: 30,
},
{
id: 3,
name: 'Color 3',
image:
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR0FyiZM7bDPsDEMtg0Zs2HXNwe2xbVh55IZA&usqp=CAU',
price: 10,
},
]
const state = useTableState({
columns: [
{
id: 'image',
header: 'Image',
resolver: {
type: 'image',
},
},
{
id: 'name',
header: 'Name',
resolver: {
type: 'text',
isNameCell: true,
mapText: (item) => item.name,
},
},
{
id: 'price',
header: 'Price',
},
],
items: fruits,
onRowClick: (item) => alert(`Item: ${item.name}`),
})
return <Table state={state} />
}
Name | Type | Description | Required |
---|---|---|---|
type | image | Image resolver type | ✅ |
alt | string | HTML img alt | 🚫 |
render | (props: ResolverRenderProps<JSX.Element, T>) => ReactNode | Resolver render function | 🚫 |
Bulk
The bulk resolver should be used when you want to add a bulk actions to the table. It is a easier way of using the BulkActions component within the Table.
When using this resolver there are two things you should follow in order to work properly: wrap the Table component with the SelectionTree
component and avoid adding more than 25 items per page.
const NUMBER_OF_ITEMS = 100
const ITEMS_PER_PAGE = 5
const defaultItems = Array(NUMBER_OF_ITEMS)
.fill()
.map((_, id) => {
return {
id: `${id}`,
name: faker.commerce.productName(),
lastSale: faker.date.past().toDateString(),
price: faker.commerce.price(),
}
})
/**
* Function to simulate a request
* You can configure the delay and numberOfItems here
*/
function request(init, end, delay = 500) {
return new Promise(function (resolve) {
setTimeout(resolve, delay, defaultItems.slice(init, end))
})
}
function Example() {
const [items, setItems] = React.useState([])
const view = useDataViewState()
const pagination = usePaginationState({
pageSize: ITEMS_PER_PAGE,
total: NUMBER_OF_ITEMS,
})
const bulk = useBulkActions({
totalItems: pagination.total,
pageItems: items,
pageSize: ITEMS_PER_PAGE,
})
const table = useTableState({
view,
columns: [
{ id: 'id', resolver: { type: 'bulk', state: bulk } },
{
id: 'name',
header: 'Product Name',
},
{
id: 'lastSale',
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
],
items,
})
React.useEffect(() => {
view.setStatus({ type: 'loading' })
request(pagination.range[0] - 1, pagination.range[1]).then((pageItems) => {
setItems(pageItems)
view.setStatus({ type: 'ready' })
})
}, [pagination.currentPage])
return (
<DataView state={view}>
<DataViewHeader>
<FlexSpacer />
<Pagination state={pagination} />
<BulkActions state={bulk}>
<Button variant="tertiary" icon={<IconPencil />}>
Edit
</Button>
<Button variant="tertiary" icon={<IconCopySimple />}>
Duplicate
</Button>
<Button variant="criticalTertiary" icon={<IconTrash />}>
Delete
</Button>
</BulkActions>
</DataViewHeader>
<SelectionTree state={bulk.selectionTree}>
<Table state={table} />
</SelectionTree>
</DataView>
)
}
render(<Example />)
Name | Type | Description | Required |
---|---|---|---|
type | bulk | Bulk resolver type | ✅ |
state | BulkActionsState<T> | The useBulkActions hook state return | ✅ |
render | (props: ResolverRenderProps<ReactNode, T>) => ReactNode | Resolver render function | 🚫 |
Selection
The selection resolver should be used when it is necessary to have rows selectable and to have control of which rows are selected.
function Example() {
const grid = useTableState({
columns: [
{
id: 'id',
resolver: {
type: 'selection',
mapId: (item) => item.id,
},
},
{
id: 'name',
header: 'Name',
resolver: {
type: 'text',
columnType: 'name',
mapText: (item) => item.name,
},
},
{
id: 'lastSale',
header: 'Last Sale',
},
{
id: 'price',
header: 'Price',
resolver: {
type: 'currency',
locale: 'en-US',
currency: 'USD',
},
},
],
length: 3,
items: React.useMemo(
() =>
Array(3)
.fill()
.map((_, id) => {
return {
id: `${id}`,
name: faker.commerce.productName(),
lastSale: faker.date.past().toDateString(),
price: faker.commerce.price(),
}
}),
[]
),
onRowClick: (item) => alert(`Item: ${item.name}`),
})
const selection = useSelectionTreeState({
items: grid.data,
mapId: (item) => item.id,
})
return (
<SelectionTree state={selection}>
<Table state={grid} />
</SelectionTree>
)
}
render(<Example />)
Name | Type | Description | Required | |
---|---|---|---|---|
type | selection | Selection resolver type | ✅ | |
mapId | `mapId: (item: T) => string | number` | The map function which returns the id to be used by the checkbox | 🚫 |
render | (props: ResolverRenderProps<ReactNode, T>) => ReactNode | Resolver render function | 🚫 |
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
Configuration
The following example allows ordering by name
, lastSale
and price
. By using the sort
property within useTableState
you can configure the sorting to match specific use cases.
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 = useTableState({
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,
sort: {
initialValue: { order: 'ASC' },
},
onRowClick: (item) => alert(`Item: ${item.name}`),
})
return <Table state={state} />
}
render(<CompareExample />)
initialValue
Defines the table's initial sorting value.
{ order?: 'ASC' | 'DESC', by?: string }
The
order
prop is related to the sorting order andby
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
andDESC
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.