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

Table

Version: 0.133.0

Table

Table is designed to render tabular data consistently for any kind of data type.

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>
)
}
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 />)
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 />)
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 />)
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 />)
/**
* 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 />)
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 />)
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 />)