Virtualized Table beta
Along with evolutionary web development, table component has always been the most popular component in our web apps especially for dashboards, data analysis. For Table V1, with even just 1000 records of data, it can be very annoying when using it, because of the poor performance.
With Virtualized Table, you can render massive chunks of data in a blink of an eye.
TIP
This component is still under testing, use at your own risk. If you find any bugs or issues, please report them at GitHub for us to fix. Also there were some APIs which are not mentioned in this documentation, some of them were not fully developed yet, which is why they are not mentioned here.
Even though Virtualized Table is efficient, when the data load is too large, your network and memory size can become the bottleneck of your app. So keep in mind that Virtualized Table is never the ultimate solution for everything, consider paginating your data, adding filters etc.
Basic usage
Let's demonstrate the performance of the Virtualized Table by rendering a basic example with 10 columns and 1000 rows.
<template>
<el-table-v2
:columns="columns"
:data="data"
:width="700"
:height="400"
fixed
/>
</template>
<script lang="ts" setup>
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 1000)
</script>
Auto resizer
When you do not want to manually pass the width
and height
properties to the table, you can wrap the table component with the AutoResizer. This will automatically update the width and height for you.
Resize your browser to see how it works.
TIP
Make sure the parent node of the AutoResizer
HAS A FIXED HEIGHT, since its default height value is set to 100%. Alternatively, you can define it by passing the style
attribute to AutoResizer
.
<template>
<div style="height: 400px">
<el-auto-resizer>
<template #default="{ height, width }">
<el-table-v2
:columns="columns"
:data="data"
:width="width"
:height="height"
fixed
/>
</template>
</el-auto-resizer>
</div>
</template>
<script lang="ts" setup>
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 200)
</script>
Customize Cell Renderer
Of course, you can render the table cell according to your needs. Here's a simple example of how to customize your cell.
<template>
<el-table-v2
:columns="columns"
:data="data"
:width="700"
:height="400"
fixed
/>
</template>
<script lang="tsx" setup>
import { ref } from 'vue'
import dayjs from 'dayjs'
import {
ElButton,
ElIcon,
ElTag,
ElTooltip,
TableV2FixedDir,
} from 'element-plus'
import { Timer } from '@element-plus/icons-vue'
import type { Column } from 'element-plus'
let id = 0
const dataGenerator = () => ({
id: `random-id-${++id}`,
name: 'Tom',
date: '2020-10-1',
})
const columns: Column<any>[] = [
{
key: 'date',
title: 'Date',
dataKey: 'date',
width: 150,
fixed: TableV2FixedDir.LEFT,
cellRenderer: ({ cellData: date }) => (
<ElTooltip content={dayjs(date).format('YYYY/MM/DD')}>
{
<span class="flex items-center">
<ElIcon class="mr-3">
<Timer />
</ElIcon>
{dayjs(date).format('YYYY/MM/DD')}
</span>
}
</ElTooltip>
),
},
{
key: 'name',
title: 'Name',
dataKey: 'name',
width: 150,
align: 'center',
cellRenderer: ({ cellData: name }) => <ElTag>{name}</ElTag>,
},
{
key: 'operations',
title: 'Operations',
cellRenderer: () => (
<>
<ElButton size="small">Edit</ElButton>
<ElButton size="small" type="danger">
Delete
</ElButton>
</>
),
width: 150,
align: 'center',
},
]
const data = ref(Array.from({ length: 200 }).map(dataGenerator))
</script>
Table with selections
Using customized cell renderer to allow selection for your table.
<template>
<div style="height: 400px">
<el-auto-resizer>
<template #default="{ height, width }">
<el-table-v2
:columns="columns"
:data="data"
:width="width"
:height="height"
fixed
/>
</template>
</el-auto-resizer>
</div>
</template>
<script lang="tsx" setup>
import { ref, unref } from 'vue'
import { ElCheckbox } from 'element-plus'
import type { FunctionalComponent } from 'vue'
import type { CheckboxValueType, Column } from 'element-plus'
type SelectionCellProps = {
value: boolean
intermediate?: boolean
onChange: (value: CheckboxValueType) => void
}
const SelectionCell: FunctionalComponent<SelectionCellProps> = ({
value,
intermediate = false,
onChange,
}) => {
return (
<ElCheckbox
onChange={onChange}
modelValue={value}
indeterminate={intermediate}
/>
)
}
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
checked: false,
parentId: null,
}
)
})
const columns: Column<any>[] = generateColumns(10)
columns.unshift({
key: 'selection',
width: 50,
cellRenderer: ({ rowData }) => {
const onChange = (value: CheckboxValueType) => (rowData.checked = value)
return <SelectionCell value={rowData.checked} onChange={onChange} />
},
headerCellRenderer: () => {
const _data = unref(data)
const onChange = (value: CheckboxValueType) =>
(data.value = _data.map((row) => {
row.checked = value
return row
}))
const allSelected = _data.every((row) => row.checked)
const containsChecked = _data.some((row) => row.checked)
return (
<SelectionCell
value={allSelected}
intermediate={containsChecked && !allSelected}
onChange={onChange}
/>
)
},
})
const data = ref(generateData(columns, 200))
</script>
Inline editing
Just as we demonstrated with selections above, you can use the same method to enable inline editing.
<template>
<div style="height: 400px">
<el-auto-resizer>
<template #default="{ height, width }">
<el-table-v2
:columns="columns"
:data="data"
:width="width"
:height="height"
fixed
/>
</template>
</el-auto-resizer>
</div>
</template>
<script lang="tsx" setup>
import { ref, withKeys } from 'vue'
import { ElInput } from 'element-plus'
import type { FunctionalComponent } from 'vue'
import type { Column, InputInstance } from 'element-plus'
type SelectionCellProps = {
value: string
intermediate?: boolean
onChange: (value: string) => void
onBlur: () => void
onKeydownEnter: () => void
forwardRef: (el: InputInstance) => void
}
const InputCell: FunctionalComponent<SelectionCellProps> = ({
value,
onChange,
onBlur,
onKeydownEnter,
forwardRef,
}) => {
return (
<ElInput
ref={forwardRef as any}
onInput={onChange}
onBlur={onBlur}
onKeydown={withKeys(onKeydownEnter, ['enter'])}
modelValue={value}
/>
)
}
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
editing: false,
parentId: null,
}
)
})
const columns: Column<any>[] = generateColumns(10)
columns[0] = {
...columns[0],
title: 'Editable Column',
cellRenderer: ({ rowData, column }) => {
const onChange = (value: string) => {
rowData[column.dataKey!] = value
}
const onEnterEditMode = () => {
rowData.editing = true
}
const onExitEditMode = () => (rowData.editing = false)
const input = ref()
const setRef = (el) => {
input.value = el
if (el) {
el.focus?.()
}
}
return rowData.editing ? (
<InputCell
forwardRef={setRef}
value={rowData[column.dataKey!]}
onChange={onChange}
onBlur={onExitEditMode}
onKeydownEnter={onExitEditMode}
/>
) : (
<div class="table-v2-inline-editing-trigger" onClick={onEnterEditMode}>
{rowData[column.dataKey!]}
</div>
)
},
}
const data = ref(generateData(columns, 200))
</script>
<style>
.table-v2-inline-editing-trigger {
border: 1px transparent dotted;
padding: 4px;
}
.table-v2-inline-editing-trigger:hover {
border-color: var(--el-color-primary);
}
</style>
Table with status
You can highlight your table content to distinguish between "success, information, warning, danger" and other states.
To customize the appearance of rows, use the row-class-name
attribute. For example, every 10th row is highlighted using the bg-blue-200
class, and every 5th row with the bg-red-100
class.
<template>
<el-table-v2
:columns="columns"
:data="data"
:row-class="rowClass"
:width="700"
:height="400"
/>
</template>
<script lang="tsx" setup>
import { ref } from 'vue'
import dayjs from 'dayjs'
import {
ElButton,
ElIcon,
ElTag,
ElTooltip,
TableV2FixedDir,
} from 'element-plus'
import { Timer } from '@element-plus/icons-vue'
import type { Column, RowClassNameGetter } from 'element-plus'
let id = 0
const dataGenerator = () => ({
id: `random-id-${++id}`,
name: 'Tom',
date: '2020-10-1',
})
const columns: Column<any>[] = [
{
key: 'date',
title: 'Date',
dataKey: 'date',
width: 150,
fixed: TableV2FixedDir.LEFT,
cellRenderer: ({ cellData: date }) => (
<ElTooltip content={dayjs(date).format('YYYY/MM/DD')}>
{
<span class="flex items-center">
<ElIcon class="mr-3">
<Timer />
</ElIcon>
{dayjs(date).format('YYYY/MM/DD')}
</span>
}
</ElTooltip>
),
},
{
key: 'name',
title: 'Name',
dataKey: 'name',
width: 150,
align: 'center',
cellRenderer: ({ cellData: name }) => <ElTag>{name}</ElTag>,
},
{
key: 'operations',
title: 'Operations',
cellRenderer: () => (
<>
<ElButton size="small">Edit</ElButton>
<ElButton size="small" type="danger">
Delete
</ElButton>
</>
),
width: 150,
align: 'center',
flexGrow: 1,
},
]
const data = ref(Array.from({ length: 200 }).map(dataGenerator))
const rowClass = ({ rowIndex }: Parameters<RowClassNameGetter<any>>[0]) => {
if (rowIndex % 10 === 5) {
return 'bg-red-100'
} else if (rowIndex % 10 === 0) {
return 'bg-blue-200'
}
return ''
}
</script>
Table with sticky rows
You can make some rows stick to the top of the table, and that can be very easily achieved by using the fixed-data
attribute.
You can dynamically set the sticky row based on scroll events, as shown in this example.
<template>
<el-table-v2
:columns="columns"
:data="tableData"
:fixed-data="fixedData"
:width="700"
:height="400"
:row-class="rowClass"
fixed
@scroll="onScroll"
/>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 200)
const rowClass = ({ rowIndex }) => {
if (rowIndex < 0 || (rowIndex + 1) % 5 === 0) return 'sticky-row'
}
const stickyIndex = ref(0)
const fixedData = computed(() =>
data.slice(stickyIndex.value, stickyIndex.value + 1)
)
const tableData = computed(() => {
return data.slice(1)
})
const onScroll = ({ scrollTop }) => {
stickyIndex.value = Math.floor(scrollTop / 250) * 5
}
</script>
<style>
.el-el-table-v2__fixed-header-row {
background-color: var(--el-color-primary-light-5);
font-weight: bold;
}
</style>
Table with fixed columns
If you want to have columns stick to the left or right for some reason, you can achieve this by adding special attributes to the table.
You can set the column's attribute fixed
to true
(representing FixedDir.LEFT
) or FixedDir.LEFT
or FixedDir.RIGHT
<template>
<el-table-v2
:columns="columns"
:data="data"
:sort-by="sortBy"
:width="700"
:height="400"
fixed
@column-sort="onSort"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { TableV2FixedDir, TableV2SortOrder } from 'element-plus'
import type { SortBy } from 'element-plus'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
let data = generateData(columns, 200)
columns[0].fixed = true
columns[1].fixed = TableV2FixedDir.LEFT
columns[9].fixed = TableV2FixedDir.RIGHT
for (let i = 0; i < 3; i++) columns[i].sortable = true
const sortBy = ref<SortBy>({
key: 'column-0',
order: TableV2SortOrder.ASC,
})
const onSort = (_sortBy: SortBy) => {
data = data.reverse()
sortBy.value = _sortBy
}
</script>
Grouping header
By customizing your header renderer, you can group your header as shown in this example.
TIP
In this case we used JSX
feature which is not supported in the playground. You may try them out in your local environment or on online IDEs such as codesandbox
.
It is recommended that you write your table component in JSX, since it contains VNode manipulations.
<template>
<el-table-v2
fixed
:columns="fixedColumns"
:data="data"
:header-height="[50, 40, 50]"
:header-class="headerClass"
:width="700"
:height="400"
>
<template #header="props">
<customized-header v-bind="props" />
</template>
</el-table-v2>
</template>
<script lang="tsx" setup>
import { TableV2FixedDir, TableV2Placeholder } from 'element-plus'
import type { FunctionalComponent } from 'vue'
import type {
HeaderClassNameGetter,
TableV2CustomizedHeaderSlotParam,
} from 'element-plus'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(15)
const data = generateData(columns, 200)
const fixedColumns = columns.map((column, columnIndex) => {
let fixed: TableV2FixedDir | undefined = undefined
if (columnIndex < 3) fixed = TableV2FixedDir.LEFT
if (columnIndex > 12) fixed = TableV2FixedDir.RIGHT
return { ...column, fixed, width: 100 }
})
const CustomizedHeader: FunctionalComponent<
TableV2CustomizedHeaderSlotParam
> = ({ cells, columns, headerIndex }) => {
if (headerIndex === 2) return cells
const groupCells = [] as typeof cells
let width = 0
let idx = 0
columns.forEach((column, columnIndex) => {
if (column.placeholderSign === TableV2Placeholder)
groupCells.push(cells[columnIndex])
else {
width += cells[columnIndex].props!.column.width
idx++
const nextColumn = columns[columnIndex + 1]
if (
columnIndex === columns.length - 1 ||
nextColumn.placeholderSign === TableV2Placeholder ||
idx === (headerIndex === 0 ? 4 : 2)
) {
groupCells.push(
<div
class="flex items-center justify-center custom-header-cell"
role="columnheader"
style={{
...cells[columnIndex].props!.style,
width: `${width}px`,
}}
>
Group width {width}
</div>
)
width = 0
idx = 0
}
}
})
return groupCells
}
const headerClass = ({
headerIndex,
}: Parameters<HeaderClassNameGetter<any>>[0]) => {
if (headerIndex === 1) return 'el-primary-color'
return ''
}
</script>
<style>
.el-el-table-v2__header-row .custom-header-cell {
border-right: 1px solid var(--el-border-color);
}
.el-el-table-v2__header-row .custom-header-cell:last-child {
border-right: none;
}
.el-primary-color {
background-color: var(--el-color-primary);
color: var(--el-color-white);
font-size: 14px;
font-weight: bold;
}
.el-primary-color .custom-header-cell {
padding: 0 4px;
}
</style>
Filter
Virtualized Table provides custom header renderers for creating customized headers. We can then utilize these to render filters.
<template>
<el-table-v2
fixed
:columns="fixedColumns"
:data="data"
:width="700"
:height="400"
/>
</template>
<script lang="tsx" setup>
import { ref } from 'vue'
import {
ElButton,
ElCheckbox,
ElIcon,
ElPopover,
TableV2FixedDir,
} from 'element-plus'
import { Filter } from '@element-plus/icons-vue'
import type { HeaderCellSlotProps } from 'element-plus'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = ref(generateData(columns, 200))
const shouldFilter = ref(false)
const popoverRef = ref()
const onFilter = () => {
popoverRef.value.hide()
if (shouldFilter.value) {
data.value = generateData(columns, 100, 'filtered-')
} else {
data.value = generateData(columns, 200)
}
}
const onReset = () => {
shouldFilter.value = false
onFilter()
}
columns[0].headerCellRenderer = (props: HeaderCellSlotProps) => {
return (
<div class="flex items-center justify-center">
<span class="mr-2 text-xs">{props.column.title}</span>
<ElPopover ref={popoverRef} trigger="click" {...{ width: 200 }}>
{{
default: () => (
<div class="filter-wrapper">
<div class="filter-group">
<ElCheckbox v-model={shouldFilter.value}>
Filter Text
</ElCheckbox>
</div>
<div class="el-table-v2__demo-filter">
<ElButton text onClick={onFilter}>
Confirm
</ElButton>
<ElButton text onClick={onReset}>
Reset
</ElButton>
</div>
</div>
),
reference: () => (
<ElIcon class="cursor-pointer">
<Filter />
</ElIcon>
),
}}
</ElPopover>
</div>
)
}
const fixedColumns = columns.map((column, columnIndex) => {
let fixed: TableV2FixedDir | undefined = undefined
if (columnIndex < 2) fixed = TableV2FixedDir.LEFT
if (columnIndex > 9) fixed = TableV2FixedDir.RIGHT
return { ...column, fixed, width: 100 }
})
</script>
<style>
.el-table-v2__demo-filter {
border-top: var(--el-border);
margin: 12px -12px -12px;
padding: 0 12px;
display: flex;
justify-content: space-between;
}
</style>
Sortable
You can sort the table with sort state.
<template>
<el-table-v2
:columns="columns"
:data="data"
:sort-by="sortState"
:width="700"
:height="400"
fixed
@column-sort="onSort"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { TableV2SortOrder } from 'element-plus'
import type { SortBy } from 'element-plus'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
let data = generateData(columns, 200)
columns[0].sortable = true
const sortState = ref<SortBy>({
key: 'column-0',
order: TableV2SortOrder.ASC,
})
const onSort = (sortBy: SortBy) => {
console.log(sortBy)
data = data.reverse()
sortState.value = sortBy
}
</script>
Controlled Sort
You can define multiple sortable columns as needed. Keep in mind that if you define multiple sortable columns, the UI may appear confusing to your users, as it becomes unclear which column is currently being sorted.
<template>
<el-table-v2
v-model:sort-state="sortState"
:columns="columns"
:data="data"
:width="700"
:height="400"
fixed
@column-sort="onSort"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { TableV2SortOrder } from 'element-plus'
import type { SortBy, SortState } from 'element-plus'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = ref(generateData(columns, 200))
columns[0].sortable = true
columns[1].sortable = true
const sortState = ref<SortState>({
'column-0': TableV2SortOrder.DESC,
'column-1': TableV2SortOrder.ASC,
})
const onSort = ({ key, order }: SortBy) => {
sortState.value[key] = order
data.value = data.value.reverse()
}
</script>
Cross hovering
When dealing with a large list, it's easy to lose track of the current row and column you are visiting. In such cases, using this feature can be very helpful.
<template>
<div style="height: 400px">
<el-auto-resizer>
<template #default="{ height, width }">
<el-table-v2
:columns="columns"
:cell-props="cellProps"
:class="kls"
:data="data"
:width="width"
:height="height"
/>
</template>
</el-auto-resizer>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
columns.unshift({
key: 'column-n-1',
width: 50,
title: 'Row No.',
cellRenderer: ({ rowIndex }) => `${rowIndex + 1}`,
align: 'center',
})
const data = generateData(columns, 200)
const cellProps = ({ columnIndex }) => {
const key = `hovering-col-${columnIndex}`
return {
['data-key']: key,
onMouseenter: () => {
kls.value = key
},
onMouseleave: () => {
kls.value = ''
},
}
}
const kls = ref<string>('')
</script>
<style>
.hovering-col-0 [data-key='hovering-col-0'],
.hovering-col-1 [data-key='hovering-col-1'],
.hovering-col-2 [data-key='hovering-col-2'],
.hovering-col-3 [data-key='hovering-col-3'],
.hovering-col-4 [data-key='hovering-col-4'],
.hovering-col-5 [data-key='hovering-col-5'],
.hovering-col-6 [data-key='hovering-col-6'],
.hovering-col-7 [data-key='hovering-col-7'],
.hovering-col-8 [data-key='hovering-col-8'],
.hovering-col-9 [data-key='hovering-col-9'],
.hovering-col-10 [data-key='hovering-col-10'] {
background: var(--el-table-row-hover-bg-color);
}
[data-key='hovering-col-0'] {
font-weight: bold;
user-select: none;
pointer-events: none;
}
</style>
Colspan
The virtualized table doesn't use the built-in table
element, so colspan
and rowspan
behave a bit differently compared to TableV1. However, with a customized row renderer, these features can still be implemented. In this section, we'll demonstrate how to achieve this.
<template>
<el-table-v2 fixed :columns="columns" :data="data" :width="700" :height="400">
<template #row="props">
<Row v-bind="props" />
</template>
</el-table-v2>
</template>
<script lang="ts" setup>
import { cloneVNode } from 'vue'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 200)
const colSpanIndex = 1
columns[colSpanIndex].colSpan = ({ rowIndex }) => (rowIndex % 4) + 1
columns[colSpanIndex].align = 'center'
const Row = ({ rowData, rowIndex, cells, columns }) => {
const colSpan = columns[colSpanIndex].colSpan({ rowData, rowIndex })
if (colSpan > 1) {
let width = Number.parseInt(cells[colSpanIndex].props.style.width)
for (let i = 1; i < colSpan; i++) {
width += Number.parseInt(cells[colSpanIndex + i].props.style.width)
cells[colSpanIndex + i] = null
}
const style = {
...cells[colSpanIndex].props.style,
width: `${width}px`,
backgroundColor: 'var(--el-color-primary-light-3)',
}
cells[colSpanIndex] = cloneVNode(cells[colSpanIndex], { style })
}
return cells
}
</script>
Rowspan
Since we have covered Colspan, it's worth noting that we also have row span. It's a little bit different from colspan but the idea is basically the same.
<template>
<el-table-v2 fixed :columns="columns" :data="data" :width="700" :height="400">
<template #row="props">
<Row v-bind="props" />
</template>
</el-table-v2>
</template>
<script lang="ts" setup>
import { cloneVNode } from 'vue'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 200)
const rowSpanIndex = 0
columns[rowSpanIndex].rowSpan = ({ rowIndex }) =>
rowIndex % 2 === 0 && rowIndex <= data.length - 2 ? 2 : 1
const Row = ({ rowData, rowIndex, cells, columns }) => {
const rowSpan = columns[rowSpanIndex].rowSpan({ rowData, rowIndex })
if (rowSpan > 1) {
const cell = cells[rowSpanIndex]
const style = {
...cell.props.style,
backgroundColor: 'var(--el-color-primary-light-3)',
height: `${rowSpan * 50 - 1}px`,
alignSelf: 'flex-start',
zIndex: 1,
}
cells[rowSpanIndex] = cloneVNode(cell, { style })
}
return cells
}
</script>
Rowspan and Colspan together
We can combine rowspan and colspan together to meet your business goal!
<template>
<el-table-v2 fixed :columns="columns" :data="data" :width="700" :height="400">
<template #row="props">
<Row v-bind="props" />
</template>
</el-table-v2>
</template>
<script lang="tsx" setup>
import { cloneVNode } from 'vue'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data = generateData(columns, 200)
const colSpanIndex = 1
columns[colSpanIndex].colSpan = ({ rowIndex }) => (rowIndex % 4) + 1
columns[colSpanIndex].align = 'center'
const rowSpanIndex = 0
columns[rowSpanIndex].rowSpan = ({ rowIndex }) =>
rowIndex % 2 === 0 && rowIndex <= data.length - 2 ? 2 : 1
const Row = ({ rowData, rowIndex, cells, columns }) => {
const colSpan = columns[colSpanIndex].colSpan({ rowData, rowIndex })
if (colSpan > 1) {
let width = Number.parseInt(cells[colSpanIndex].props.style.width)
for (let i = 1; i < colSpan; i++) {
width += Number.parseInt(cells[colSpanIndex + i].props.style.width)
cells[colSpanIndex + i] = null
}
const style = {
...cells[colSpanIndex].props.style,
width: `${width}px`,
backgroundColor: 'var(--el-color-primary-light-3)',
}
cells[colSpanIndex] = cloneVNode(cells[colSpanIndex], { style })
}
const rowSpan = columns[rowSpanIndex].rowSpan({ rowData, rowIndex })
if (rowSpan > 1) {
const cell = cells[rowSpanIndex]
const style = {
...cell.props.style,
backgroundColor: 'var(--el-color-danger-light-3)',
height: `${rowSpan * 50}px`,
alignSelf: 'flex-start',
zIndex: 1,
}
cells[rowSpanIndex] = cloneVNode(cell, { style })
} else {
const style = cells[rowSpanIndex].props.style
// override the cell here for creating a pure node without pollute the style
cells[rowSpanIndex] = (
<div style={{ ...style, width: `${style.width}px` }} />
)
}
return cells
}
</script>
Tree data
Virtual Table can also render data in a tree-like structure. By clicking the arrow icon, you can expand or collapse the tree nodes.
<template>
<el-table-v2
v-model:expanded-row-keys="expandedRowKeys"
:columns="columns"
:data="treeData"
:width="700"
:expand-column-key="expandColumnKey"
:height="400"
fixed
@row-expand="onRowExpanded"
@expanded-rows-change="onExpandedRowsChange"
/>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { TableV2FixedDir } from 'element-plus'
import type { ExpandedRowsChangeHandler, RowExpandHandler } from 'element-plus'
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({ length }).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({ length }).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10).map((column, columnIndex) => {
let fixed!: TableV2FixedDir
if (columnIndex < 2) fixed = TableV2FixedDir.LEFT
if (columnIndex > 8) fixed = TableV2FixedDir.RIGHT
return { ...column, fixed }
})
const data = generateData(columns, 200)
const expandColumnKey = 'column-0'
// add some sub items
for (let i = 0; i < 50; i++) {
data.push(
{
...data[0],
id: `${data[0].id}-sub-${i}`,
parentId: data[0].id,
[expandColumnKey]: `Sub ${i}`,
},
{
...data[2],
id: `${data[2].id}-sub-${i}`,
parentId: data[2].id,
[expandColumnKey]: `Sub ${i}`,
},
{
...data[2],
id: `${data[2].id}-sub-sub-${i}`,
parentId: `${data[2].id}-sub-${i}`,
[expandColumnKey]: `Sub-Sub ${i}`,
}
)
}
function unflatten(
data: ReturnType<typeof generateData>,
rootId = null,
dataKey = 'id',
parentKey = 'parentId'
) {
const tree: any[] = []
const childrenMap = {}
for (const datum of data) {
const item = { ...datum }
const id = item[dataKey]
const parentId = item[parentKey]
if (Array.isArray(item.children)) {
childrenMap[id] = item.children.concat(childrenMap[id] || [])
} else if (!childrenMap[id]) {
childrenMap[id] = []
}
item.children = childrenMap[id]
if (parentId !== undefined && parentId !== rootId) {
if (!childrenMap[parentId]) childrenMap[parentId] = []
childrenMap[parentId].push(item)
} else {
tree.push(item)
}
}
return tree
}
const treeData = computed(() => unflatten(data))
const expandedRowKeys = ref<string[]>([])
const onRowExpanded = ({ expanded }: Parameters<RowExpandHandler<any>>[0]) => {
console.log('Expanded:', expanded)
}
const onExpandedRowsChange = (
expandedKeys: Parameters<ExpandedRowsChangeHandler>[0]
) => {
console.log(expandedKeys)
}
</script>
Dynamic height rows
Virtual Table is capable of rendering rows with dynamic heights. If you're working with data and are uncertain about the content size, this feature is ideal for rendering rows that adjust to the content's height. To enable this, pass down the estimated-row-height
attribute. The closer the estimated height matches the actual content, the smoother the rendering experience.
TIP
Each row's height is dynamically measured during rendering the rows. As a result, if you're trying to display a large amount of data, the UI might be bouncing.