Skip to content
🌏 Translated with the assistance of DeepSeek and ChatGPT

Table

This is a table used to display content with rows and columns.

WARNING

To ensure the row expansion and row selection functions work properly, when these functions are needed, you must ensure that each data item in the data array contains a field with the same name as the rowKey parameter (default is 'key'), and the value of this field is unique across all data.

If backend pagination is needed, please refer to Server-side Pagination.

If cross-page selection is needed, please refer to Cross-page Select all.

Basic Usage

The data prop is the current data of the table, and the columns prop sets the table columns.

Please configure the key property for elements in columns as the unique identifier for rows.

Please configure the key property for elements in data as the unique identifier for rows; this unique identifier field can be controlled by rowKey.

Name
Age
Email
Alice
25
alice@example.com
Bob
30
bob@example.com
Charlie
35
charlie@example.com
1
<template>
	<px-table :data="data" :columns="columns"></px-table>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
	{ key: 1, name: 'Alice', age: 25, email: 'alice@example.com' },
	{ key: 2, name: 'Bob', age: 30, email: 'bob@example.com' },
	{ key: 3, name: 'Charlie', age: 35, email: 'charlie@example.com' }
])

const columns = [
	{
		key: 'name',
		label: 'Name',
		field: 'name'
	},
	{
		key: 'age',
		label: 'Age',
		field: 'age'
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email'
	}
]
</script>

Alignment

columns[].align configures the text alignment of cells; the default is 'left'.

Name
Age
Email
Olivia
28
olivia@example.com
James
32
james@example.com
Sophia
24
sophia@example.com
William
29
william@example.com
Emma
31
emma@example.com
1
<template>
	<px-table :data="data" :columns="columns"></px-table>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
	{ key: 1, name: 'Olivia', age: 28, email: 'olivia@example.com' },
	{ key: 2, name: 'James', age: 32, email: 'james@example.com' },
	{ key: 3, name: 'Sophia', age: 24, email: 'sophia@example.com' },
	{ key: 4, name: 'William', age: 29, email: 'william@example.com' },
	{ key: 5, name: 'Emma', age: 31, email: 'emma@example.com' }
])

const columns = [
	{
		key: 'name',
		label: 'Name',
		field: 'name',
		align: 'left'
	},
	{
		key: 'age',
		label: 'Age',
		field: 'age',
		align: 'center'
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email',
		align: 'right'
	}
]
</script>

Custom Cells

You can customize cells in the following ways:

  • Use columns[].slotName to configure a cell content slot.
  • Use columns[].labelSlotName to configure a header cell content slot.
  • Use columns[].render to configure a cell content render function.
  • Use columns[].labelRender to configure a header cell render function.
  • Use columns[].cellProps to configure cell properties.
  • Use columns[].labelCellProps to configure header cell properties.
  • Use columns[].contentProps to configure cell content properties.
  • Use columns[].labelContentProps to configure header content properties.
Name
Age
Email
Status Active / Inactive
Actions
Emma Johnson
32
emma.johnson@example.com
active
James Miller
45
jmiller@test.net
inactive
Sophia Chen
28
schen@demo.org
active
William Brown
61
wbrown55@sample.co
inactive
Olivia Davis
39
odavis@mail.io
active
1
<template>
	<px-table :data="data" :columns="columns">
		<template #status="{ record }">
			<px-tag :theme="record.status === 'active' ? 'success' : 'danger'">{{
				record.status
			}}</px-tag>
		</template>
		<template #status-label>
			Status
			<px-tag style="margin-left: 8px" theme="success">Active</px-tag>
			/
			<px-tag theme="danger">Inactive</px-tag>
		</template>
	</px-table>
</template>

<script setup lang="ts">
import { Button } from '@pixelium/web-vue'
import { h, ref } from 'vue'

const data = ref([
	{
		key: 1,
		name: 'Emma Johnson',
		age: 32,
		email: 'emma.johnson@example.com',
		status: 'active'
	},
	{
		key: 2,
		name: 'James Miller',
		age: 45,
		email: 'jmiller@test.net',
		status: 'inactive'
	},
	{
		key: 3,
		name: 'Sophia Chen',
		age: 28,
		email: 'schen@demo.org',
		status: 'active'
	},
	{
		key: 4,
		name: 'William Brown',
		age: 61,
		email: 'wbrown55@sample.co',
		status: 'inactive'
	},
	{
		key: 5,
		name: 'Olivia Davis',
		age: 39,
		email: 'odavis@mail.io',
		status: 'active'
	}
])

const columns = [
	{
		key: 'name',
		label: 'Name',
		field: 'name'
	},
	{
		key: 'age',
		label: 'Age',
		field: 'age'
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email',
		cellProps: {
			style: {
				backgroundColor: 'wheat',
				color: 'var(--px-neutral-10)'
			}
		}
	},
	{
		key: 'status',
		labelSlotName: 'status-label',
		slotName: 'status'
	},
	{
		key: 'action',
		render: () => {
			return h(Button, { size: 'small' }, { default: () => 'Detail' })
		},
		labelRender: () => {
			return 'Actions'
		}
	}
]
</script>

Column Width

columns[].width and columns[].minWidth configure cell width.

When setting columns[].width, please leave at least one column unset so the table can adapt to its actual width.

Name
Role
Email
Address
Olivia Martinez
Admin
olivia.martinez@company.com
123 Maple Street, New York, NY 10001
James Wilson
Developer
james.wilson@company.com
456 Oak Avenue, San Francisco, CA 94102
Sophia Chen
Designer
sophia.chen@company.com
789 Pine Road, Chicago, IL 60601
William Taylor
Manager
william.taylor@company.com
321 Elm Boulevard, Austin, TX 73301
Emma Johnson
Analyst
emma.johnson@company.com
654 Cedar Lane, Seattle, WA 98101
1
<template>
	<px-table :data="data" :columns="columns"></px-table>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
	{
		key: 1,
		name: 'Olivia Martinez',
		role: 'Admin',
		email: 'olivia.martinez@company.com',
		address: '123 Maple Street, New York, NY 10001'
	},
	{
		key: 2,
		name: 'James Wilson',
		role: 'Developer',
		email: 'james.wilson@company.com',
		address: '456 Oak Avenue, San Francisco, CA 94102'
	},
	{
		key: 3,
		name: 'Sophia Chen',
		role: 'Designer',
		email: 'sophia.chen@company.com',
		address: '789 Pine Road, Chicago, IL 60601'
	},
	{
		key: 4,
		name: 'William Taylor',
		role: 'Manager',
		email: 'william.taylor@company.com',
		address: '321 Elm Boulevard, Austin, TX 73301'
	},
	{
		key: 5,
		name: 'Emma Johnson',
		role: 'Analyst',
		email: 'emma.johnson@company.com',
		address: '654 Cedar Lane, Seattle, WA 98101'
	}
])

const columns = [
	{
		key: 'name',
		label: 'Name',
		field: 'name',
		minWidth: 250
	},
	{
		key: 'role',
		label: 'Role',
		field: 'role',
		width: 120
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email',
		minWidth: 300
	},
	{
		key: 'address',
		label: 'Address',
		field: 'address',
		minWidth: 300
	}
]
</script>

Borders and Background

bordered enables table borders; by default all borders are shown. variant sets the table background style variant; the default is a solid background ('normal').

Bordered:
Variant:
Name
Age
Email
Alice
25
alice@example.com
Bob
30
bob@example.com
Charlie
35
charlie@example.com
1
<template>
	<px-space direction="vertical">
		<div style="display: flex; align-items: center">
			<px-checkbox v-model="threeLineTable" @change="threeLineTableCheckChangeHandler">
				Three-Line Table
			</px-checkbox>
		</div>
		<div style="display: flex; align-items: center">
			Bordered:
			<px-checkbox-group
				style="margin-left: 16px"
				v-model="bordered"
				:options="borderedOptions"
				@change="borderedCheckChangeHandler"
			></px-checkbox-group>
		</div>
		<div style="display: flex; align-items: center">
			Variant:
			<px-radio-group
				style="margin-left: 16px"
				v-model="variant"
				:options="variantOptions"
			></px-radio-group>
		</div>
		<px-table
			:data="data"
			:columns="columns"
			:variant="variant"
			:bordered="borderedConfig"
		></px-table>
	</px-space>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'

const data = ref([
	{ key: 1, name: 'Alice', age: 25, email: 'alice@example.com' },
	{ key: 2, name: 'Bob', age: 30, email: 'bob@example.com' },
	{ key: 3, name: 'Charlie', age: 35, email: 'charlie@example.com' }
])

const columns = [
	{
		key: 'name',
		label: 'Name',
		field: 'name'
	},
	{
		key: 'age',
		label: 'Age',
		field: 'age'
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email'
	}
]

const borderedOptions = [
	{ label: 'Table', value: 'table' },
	{ label: 'Side', value: 'side' },
	{ label: 'Head', value: 'head' },
	{ label: 'Row', value: 'row' },
	{ label: 'Col', value: 'col' }
]
const bordered = ref<string[]>(['table', 'head'])
const threeLineTable = ref(true)

const borderedCheckChangeHandler = (val: string[]) => {
	if (val.length === 2 && val.includes('table') && val.includes('head')) {
		threeLineTable.value = true
	} else {
		threeLineTable.value = false
	}
}

const threeLineTableCheckChangeHandler = (val: boolean) => {
	if (val) {
		bordered.value = ['table', 'head']
	}
}

const borderedConfig = computed(() => {
	return {
		table: bordered.value.includes('table'),
		side: bordered.value.includes('side'),
		head: bordered.value.includes('head'),
		row: bordered.value.includes('row'),
		col: bordered.value.includes('col')
	}
})

const variantOptions = [
	{ label: 'Normal', value: 'normal' },
	{ label: 'Striped', value: 'striped' },
	{ label: 'Checkered', value: 'checkered' }
]
const variant = ref<string>('normal')
</script>

Due to the features of display: table, unexpected results may occur when setting a small height or max-height on the outermost element of the Table component. The <table> height will still be expanded by its child elements.

We recommend using the tableAreaProps property to set a fixed or dynamic table height, for example: :table-area-props="{ style: 'max-height: 400px' }".

Name
Role
Level
Salary
Address
Aric Ironfist
Blacksmith
42
2800
Iron Forge Street 12
Lyra Swiftarrow
Hunter
36
2200
Forest Edge Cottage
Borin Stonebeard
Miner
28
1800
Mountain Hall 7
Elara Moonshadow
Alchemist
51
3200
Apothecary Lane 3
Garrick Stormblade
Knight
65
4500
Castle Barracks
Mira Whisperwind
Bard
24
1500
Tavern Square 5
Thrain Forgeheart
Engineer
47
2900
Workshop District 9
Sylas Greenleaf
Druid
58
3800
Ancient Grove
Kaelen Firehand
Pyromancer
61
4200
Mage Tower East
Freya Shieldmaiden
Guard Captain
54
3600
Guard Post Central
Jorin Goldfinder
Merchant
33
2500
Market Square 8
Nyssa Lightfoot
Rogue
39
2700
Shadow Alley 2
Torin Boulderbreaker
Stone Mason
31
2100
Quarry Road 4
Liana Starweaver
Astrologer
45
3100
Observatory Hill
Finn Riverstride
Fisherman
22
1400
Dockside Hut
Rowan Oakenshield
Carpenter
27
1700
Timber Yard 6
Vera Nightshade
Herbalist
34
2300
Herb Garden Cottage
Draven Bloodedge
Mercenary
49
3300
Fighter's Guild
Mara Swiftcurrent
Sailor
26
1600
Harbor Watch 3
Orin Deepdelver
Archaeologist
43
3000
Explorer's Lodge
<template>
	<px-table
		:pagination="false"
		:data="data"
		:columns="columns"
		row-key="name"
		:table-area-props="{ style: 'max-height: 400px' }"
	></px-table>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
	{
		name: 'Aric Ironfist',
		role: 'Blacksmith',
		level: 42,
		salary: 2800,
		address: 'Iron Forge Street 12'
	},
	{
		name: 'Lyra Swiftarrow',
		role: 'Hunter',
		level: 36,
		salary: 2200,
		address: 'Forest Edge Cottage'
	},
	{
		name: 'Borin Stonebeard',
		role: 'Miner',
		level: 28,
		salary: 1800,
		address: 'Mountain Hall 7'
	},
	{
		name: 'Elara Moonshadow',
		role: 'Alchemist',
		level: 51,
		salary: 3200,
		address: 'Apothecary Lane 3'
	},
	{
		name: 'Garrick Stormblade',
		role: 'Knight',
		level: 65,
		salary: 4500,
		address: 'Castle Barracks'
	},
	{
		name: 'Mira Whisperwind',
		role: 'Bard',
		level: 24,
		salary: 1500,
		address: 'Tavern Square 5'
	},
	{
		name: 'Thrain Forgeheart',
		role: 'Engineer',
		level: 47,
		salary: 2900,
		address: 'Workshop District 9'
	},
	{
		name: 'Sylas Greenleaf',
		role: 'Druid',
		level: 58,
		salary: 3800,
		address: 'Ancient Grove'
	},
	{
		name: 'Kaelen Firehand',
		role: 'Pyromancer',
		level: 61,
		salary: 4200,
		address: 'Mage Tower East'
	},
	{
		name: 'Freya Shieldmaiden',
		role: 'Guard Captain',
		level: 54,
		salary: 3600,
		address: 'Guard Post Central'
	},
	{
		name: 'Jorin Goldfinder',
		role: 'Merchant',
		level: 33,
		salary: 2500,
		address: 'Market Square 8'
	},
	{
		name: 'Nyssa Lightfoot',
		role: 'Rogue',
		level: 39,
		salary: 2700,
		address: 'Shadow Alley 2'
	},
	{
		name: 'Torin Boulderbreaker',
		role: 'Stone Mason',
		level: 31,
		salary: 2100,
		address: 'Quarry Road 4'
	},
	{
		name: 'Liana Starweaver',
		role: 'Astrologer',
		level: 45,
		salary: 3100,
		address: 'Observatory Hill'
	},
	{
		name: 'Finn Riverstride',
		role: 'Fisherman',
		level: 22,
		salary: 1400,
		address: 'Dockside Hut'
	},
	{
		name: 'Rowan Oakenshield',
		role: 'Carpenter',
		level: 27,
		salary: 1700,
		address: 'Timber Yard 6'
	},
	{
		name: 'Vera Nightshade',
		role: 'Herbalist',
		level: 34,
		salary: 2300,
		address: 'Herb Garden Cottage'
	},
	{
		name: 'Draven Bloodedge',
		role: 'Mercenary',
		level: 49,
		salary: 3300,
		address: "Fighter's Guild"
	},
	{
		name: 'Mara Swiftcurrent',
		role: 'Sailor',
		level: 26,
		salary: 1600,
		address: 'Harbor Watch 3'
	},
	{
		name: 'Orin Deepdelver',
		role: 'Archaeologist',
		level: 43,
		salary: 3000,
		address: "Explorer's Lodge"
	}
])

const columns = [
	{
		key: 'name',
		label: 'Name',
		field: 'name'
	},
	{
		key: 'role',
		label: 'Role',
		field: 'role'
	},
	{
		key: 'level',
		label: 'Level',
		field: 'level'
	},
	{
		key: 'salary',
		label: 'Salary',
		field: 'salary'
	},
	{
		label: 'Address',
		field: 'address',
		key: 'address'
	}
]
</script>

Scroll Area

scroll.x configures the scroll area's width. The Table component internally computes a minimum scroll width based on column configuration; when the table's actual width is less than the greater of that computed minimum and scroll.x, a horizontal scrollbar will appear.

Name
Age
Email
Alice
25
alice@example.com
Bob
30
bob@example.com
Charlie
35
charlie@example.com
1
<template>
	<px-table :data="data" :columns="columns" :scroll="{ x: 1200 }"></px-table>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
	{ key: 1, name: 'Alice', age: 25, email: 'alice@example.com' },
	{ key: 2, name: 'Bob', age: 30, email: 'bob@example.com' },
	{ key: 3, name: 'Charlie', age: 35, email: 'charlie@example.com' }
])

const columns = [
	{
		key: 'name',
		label: 'Name',
		field: 'name'
	},
	{
		key: 'age',
		label: 'Age',
		field: 'age'
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email'
	}
]
</script>

Cell Merging

Configure merged cells using the spanMethod property.

BuildingType
Coord
Level
Income
Maintain
Blacksmith
(23, 18)
3
1800
450
(25, 20)
4
2400
600
Tavern
(34, 27)
2
1600
400
(36, 25)
5
3200
800
Market
(41, 33)
3
2100
525
(43, 35)
6
4000
1000
Farm
(15, 12)
2
900
225
(18, 14)
4
2000
500
Barracks
(9, 39)
3
1500
375
(11, 37)
5
2800
700
1
2
<template>
	<px-table :data="data" :columns="columns" :spanMethod="spanMethod"></px-table>
</template>

<script setup lang="ts">
import { ref } from 'vue'

import type { TableOptionsArg } from '@pixelium/web-vue'
// If on-demand import
// import type { TableOptionsArg } from '@pixelium/web-vue/es'

const data = ref([
	{
		buildingType: 'Blacksmith',
		coord: '(23, 18)',
		level: 3,
		income: 1800,
		maintain: 450
	},
	{
		buildingType: 'Blacksmith',
		coord: '(25, 20)',
		level: 4,
		income: 2400,
		maintain: 600
	},
	{
		buildingType: 'Tavern',
		coord: '(34, 27)',
		level: 2,
		income: 1600,
		maintain: 400
	},
	{
		buildingType: 'Tavern',
		coord: '(36, 25)',
		level: 5,
		income: 3200,
		maintain: 800
	},
	{
		buildingType: 'Market',
		coord: '(41, 33)',
		level: 3,
		income: 2100,
		maintain: 525
	},
	{
		buildingType: 'Market',
		coord: '(43, 35)',
		level: 6,
		income: 4000,
		maintain: 1000
	},
	{
		buildingType: 'Farm',
		coord: '(15, 12)',
		level: 2,
		income: 900,
		maintain: 225
	},
	{
		buildingType: 'Farm',
		coord: '(18, 14)',
		level: 4,
		income: 2000,
		maintain: 500
	},
	{
		buildingType: 'Barracks',
		coord: '(9, 39)',
		level: 3,
		income: 1500,
		maintain: 375
	},
	{
		buildingType: 'Barracks',
		coord: '(11, 37)',
		level: 5,
		income: 2800,
		maintain: 700
	},
	{
		buildingType: 'Town Hall',
		coord: '(12, 45)',
		level: 5
	}
])

const columns = [
	{
		key: 'buildingType',
		label: 'BuildingType',
		field: 'buildingType'
	},
	{
		key: 'coord',
		label: 'Coord',
		field: 'coord'
	},
	{
		key: 'level',
		label: 'Level',
		field: 'level'
	},
	{
		key: 'income',
		label: 'Income',
		field: 'income'
	},
	{
		label: 'Maintain',
		field: 'maintain',
		key: 'maintain'
	}
]

const spanMethod = ({ rowIndex, colIndex, record, column }: TableOptionsArg) => {
	const curBuildingType = record.buildingType
	if (colIndex === 0) {
		if (data.value[rowIndex - 1]?.buildingType === curBuildingType) {
			return
		}
		let rowspan = 0
		for (let i = rowIndex; i < data.value.length; i++) {
			if (data.value[i].buildingType !== curBuildingType) {
				break
			}
			rowspan++
		}
		return {
			rowspan
		}
	}
	if (curBuildingType === 'Town Hall' && colIndex === 2) {
		return { colspan: 3 }
	}
}
</script>

Fixed Header and Fixed Columns

The table header is fixed by default; it can be configured with the fixedHead prop.

Set fixed: 'left' or fixed: 'right' on child elements of the columns prop to fix columns.

WARNING

Fixed columns, when rendered, will move to the corresponding fixed side if they are in the middle of the table.

Fixed column configuration only applies to root nodes in the case of multi-level headers.

Id
Name
Age
Phone
Email
Address
1
John Smith
28
+1-555-101-2001
john.smith@example.com
123 Main St, Springfield, IL 62704
2
Jane Johnson
34
+1-555-102-2002
jane.johnson@example.com
456 Oak Ave, Shelbyville, IL 62706
3
Bob Williams
22
+1-555-103-2003
bob.williams@example.com
789 Pine Rd, Capital City, IL 62708
4
Alice Brown
45
+1-555-104-2004
alice.brown@example.com
101 Maple Dr, Metropolis, IL 62710
5
Charlie Jones
31
+1-555-105-2005
charlie.jones@example.com
202 Cedar Ln, Smallville, IL 62712
6
Diana Garcia
29
+1-555-106-2006
diana.garcia@example.com
303 Birch St, Gotham, NY 10001
7
Eve Miller
38
+1-555-107-2007
eve.miller@example.com
404 Elm Pl, Star City, NY 10003
8
Frank Davis
26
+1-555-108-2008
frank.davis@example.com
505 Walnut Way, Central City, NY 10005
9
Grace Rodriguez
41
+1-555-109-2009
grace.rodriguez@example.com
606 Spruce Ct, Coast City, CA 90210
10
Henry Martinez
33
+1-555-110-2010
henry.martinez@example.com
707 Ash Blvd, Emerald City, CA 90212
11
Ivy Hernandez
27
+1-555-111-2011
ivy.hernandez@example.com
808 Poplar Ave, Keystone City, CA 90214
12
Jack Lopez
36
+1-555-112-2012
jack.lopez@example.com
909 Fir St, National City, TX 75001
13
Kate Gonzalez
24
+1-555-113-2013
kate.gonzalez@example.com
111 Redwood Dr, Opal City, TX 75003
14
Leo Wilson
39
+1-555-114-2014
leo.wilson@example.com
222 Sequoia Ln, Ivy Town, TX 75005
15
Mia Anderson
30
+1-555-115-2015
mia.anderson@example.com
333 Magnolia Rd, Hub City, FL 32003
16
Nick Thomas
42
+1-555-116-2016
nick.thomas@example.com
444 Willow Cir, Blue Valley, FL 32005
17
Olivia Taylor
35
+1-555-117-2017
olivia.taylor@example.com
555 Cypress St, Starling City, FL 32007
18
Paul Moore
23
+1-555-118-2018
paul.moore@example.com
666 Juniper Way, Fawcett City, WA 98001
19
Quinn Jackson
37
+1-555-119-2019
quinn.jackson@example.com
777 Sycamore Pl, Gateway City, WA 98003
20
Ryan Martin
32
+1-555-120-2020
ryan.martin@example.com
888 Laurel Ave, Coral City, WA 98005
<template>
	<px-table
		:data="data"
		:columns="columns"
		row-key="id"
		:table-area-props="{ style: 'max-height: 400px' }"
		:scroll="{ x: 1200 }"
		:pagination="false"
	></px-table>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
	{
		id: 1,
		name: 'John Smith',
		age: 28,
		phone: '+1-555-101-2001',
		email: 'john.smith@example.com',
		address: '123 Main St, Springfield, IL 62704'
	},
	{
		id: 2,
		name: 'Jane Johnson',
		age: 34,
		phone: '+1-555-102-2002',
		email: 'jane.johnson@example.com',
		address: '456 Oak Ave, Shelbyville, IL 62706'
	},
	{
		id: 3,
		name: 'Bob Williams',
		age: 22,
		phone: '+1-555-103-2003',
		email: 'bob.williams@example.com',
		address: '789 Pine Rd, Capital City, IL 62708'
	},
	{
		id: 4,
		name: 'Alice Brown',
		age: 45,
		phone: '+1-555-104-2004',
		email: 'alice.brown@example.com',
		address: '101 Maple Dr, Metropolis, IL 62710'
	},
	{
		id: 5,
		name: 'Charlie Jones',
		age: 31,
		phone: '+1-555-105-2005',
		email: 'charlie.jones@example.com',
		address: '202 Cedar Ln, Smallville, IL 62712'
	},
	{
		id: 6,
		name: 'Diana Garcia',
		age: 29,
		phone: '+1-555-106-2006',
		email: 'diana.garcia@example.com',
		address: '303 Birch St, Gotham, NY 10001'
	},
	{
		id: 7,
		name: 'Eve Miller',
		age: 38,
		phone: '+1-555-107-2007',
		email: 'eve.miller@example.com',
		address: '404 Elm Pl, Star City, NY 10003'
	},
	{
		id: 8,
		name: 'Frank Davis',
		age: 26,
		phone: '+1-555-108-2008',
		email: 'frank.davis@example.com',
		address: '505 Walnut Way, Central City, NY 10005'
	},
	{
		id: 9,
		name: 'Grace Rodriguez',
		age: 41,
		phone: '+1-555-109-2009',
		email: 'grace.rodriguez@example.com',
		address: '606 Spruce Ct, Coast City, CA 90210'
	},
	{
		id: 10,
		name: 'Henry Martinez',
		age: 33,
		phone: '+1-555-110-2010',
		email: 'henry.martinez@example.com',
		address: '707 Ash Blvd, Emerald City, CA 90212'
	},
	{
		id: 11,
		name: 'Ivy Hernandez',
		age: 27,
		phone: '+1-555-111-2011',
		email: 'ivy.hernandez@example.com',
		address: '808 Poplar Ave, Keystone City, CA 90214'
	},
	{
		id: 12,
		name: 'Jack Lopez',
		age: 36,
		phone: '+1-555-112-2012',
		email: 'jack.lopez@example.com',
		address: '909 Fir St, National City, TX 75001'
	},
	{
		id: 13,
		name: 'Kate Gonzalez',
		age: 24,
		phone: '+1-555-113-2013',
		email: 'kate.gonzalez@example.com',
		address: '111 Redwood Dr, Opal City, TX 75003'
	},
	{
		id: 14,
		name: 'Leo Wilson',
		age: 39,
		phone: '+1-555-114-2014',
		email: 'leo.wilson@example.com',
		address: '222 Sequoia Ln, Ivy Town, TX 75005'
	},
	{
		id: 15,
		name: 'Mia Anderson',
		age: 30,
		phone: '+1-555-115-2015',
		email: 'mia.anderson@example.com',
		address: '333 Magnolia Rd, Hub City, FL 32003'
	},
	{
		id: 16,
		name: 'Nick Thomas',
		age: 42,
		phone: '+1-555-116-2016',
		email: 'nick.thomas@example.com',
		address: '444 Willow Cir, Blue Valley, FL 32005'
	},
	{
		id: 17,
		name: 'Olivia Taylor',
		age: 35,
		phone: '+1-555-117-2017',
		email: 'olivia.taylor@example.com',
		address: '555 Cypress St, Starling City, FL 32007'
	},
	{
		id: 18,
		name: 'Paul Moore',
		age: 23,
		phone: '+1-555-118-2018',
		email: 'paul.moore@example.com',
		address: '666 Juniper Way, Fawcett City, WA 98001'
	},
	{
		id: 19,
		name: 'Quinn Jackson',
		age: 37,
		phone: '+1-555-119-2019',
		email: 'quinn.jackson@example.com',
		address: '777 Sycamore Pl, Gateway City, WA 98003'
	},
	{
		id: 20,
		name: 'Ryan Martin',
		age: 32,
		phone: '+1-555-120-2020',
		email: 'ryan.martin@example.com',
		address: '888 Laurel Ave, Coral City, WA 98005'
	}
])

const columns = [
	{
		key: 'id',
		label: 'Id',
		field: 'id',
		fixed: 'left'
	},
	{
		key: 'name',
		label: 'Name',
		field: 'name'
	},
	{
		key: 'age',
		label: 'Age',
		field: 'age'
	},
	{
		key: 'phone',
		label: 'Phone',
		field: 'phone'
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email'
	},
	{
		label: 'Address',
		field: 'address',
		key: 'address',
		fixed: 'right',
		width: 300
	}
]
</script>

Multi-level Headers

Set children on child elements of the columns prop to enable multi-level headers.

In multi-level header cases, the header area shows cell borders.

Basic Information
Contact Information
Work Information
Status
ID
Name
Contact Details
Address
Department
Salary Information
Phone
Email
Base Salary
Bonus
Total
1
John Smith
+1 (212) 555-0198
john.smith@example.com
123 Main St, New York, NY
Sales
5000
1000
6000
Active
2
Emily Johnson
+44 20 7946 0958
emily.j@example.com
456 Park Ave, Los Angeles, CA
Marketing
4500
800
5300
Active
3
Michael Brown
+61 2 9876 5432
m.brown@example.com
789 Broadway, Chicago, IL
Engineering
7000
1500
8500
Active
4
Sarah Davis
+49 30 12345678
sarah.davis@example.com
101 Oak St, Houston, TX
HR
4800
600
5400
On Leave
5
David Wilson
+33 1 2345 6789
d.wilson@example.com
202 Pine Rd, Phoenix, AZ
Finance
6500
1200
7700
Active
6
Jennifer Taylor
+81 3 1234 5678
jennifer.t@example.com
303 Elm St, Philadelphia, PA
Sales
5200
900
6100
Active
7
Robert Miller
+1 (415) 555-0134
robert.m@example.com
404 Cedar Ave, San Antonio, TX
Engineering
7200
1600
8800
Active
8
Lisa Anderson
+44 131 496 0600
lisa.a@example.com
505 Maple Dr, San Diego, CA
Marketing
4700
700
5400
Inactive
9
William Thomas
+61 8 8123 4567
william.t@example.com
606 Birch Ln, Dallas, TX
Finance
6800
1300
8100
Active
10
Maria Jackson
+49 89 98765432
maria.j@example.com
707 Walnut St, San Jose, CA
HR
4900
650
5550
Active
1
<template>
	<px-table
		:data="data"
		:columns="columns"
		:scroll="{ x: 1500 }"
		:table-area-props="{ style: 'max-height: 400px' }"
	></px-table>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const columns = [
	{
		label: 'Basic Information',
		align: 'center',
		key: 'base-info',
		children: [
			{
				label: 'ID',
				field: 'id',
				width: 80,
				align: 'center',
				key: 'id'
			},
			{
				label: 'Name',
				field: 'name',
				width: 120,
				key: 'name'
			}
		]
	},
	{
		label: 'Contact Information',
		key: 'contactInformation',
		children: [
			{
				label: 'Contact Details',
				key: 'contactDetails',
				children: [
					{
						key: 'phone',
						label: 'Phone',
						field: 'phone',
						minWidth: 120
					},
					{
						key: 'email',
						label: 'Email',
						field: 'email',
						minWidth: 180
					}
				]
			},
			{
				label: 'Address',
				field: 'address',
				key: 'address',
				minWidth: 200
			}
		]
	},
	{
		label: 'Work Information',
		key: 'workInformation',
		children: [
			{
				label: 'Department',
				field: 'department',
				key: 'department',
				width: 120
			},
			{
				label: 'Salary Information',
				key: 'salaryInformation',
				children: [
					{
						label: 'Base Salary',
						field: 'baseSalary',
						key: 'baseSalary',
						width: 100,
						align: 'right'
					},
					{
						label: 'Bonus',
						field: 'bonus',
						key: 'bonus',
						width: 100,
						align: 'right'
					},
					{
						label: 'Total',
						field: 'totalSalary',
						key: 'totalSalary',
						width: 100,
						align: 'right'
					}
				]
			}
		]
	},
	{
		label: 'Status',
		field: 'status',
		width: 80,
		fixed: 'right',
		key: 'status',
		align: 'center'
	}
]

const data = ref([
	{
		id: 1,
		name: 'John Smith',
		phone: '+1 (212) 555-0198',
		email: 'john.smith@example.com',
		address: '123 Main St, New York, NY',
		department: 'Sales',
		baseSalary: 5000,
		bonus: 1000,
		totalSalary: 6000,
		status: 'Active'
	},
	{
		id: 2,
		name: 'Emily Johnson',
		phone: '+44 20 7946 0958',
		email: 'emily.j@example.com',
		address: '456 Park Ave, Los Angeles, CA',
		department: 'Marketing',
		baseSalary: 4500,
		bonus: 800,
		totalSalary: 5300,
		status: 'Active'
	},
	{
		id: 3,
		name: 'Michael Brown',
		phone: '+61 2 9876 5432',
		email: 'm.brown@example.com',
		address: '789 Broadway, Chicago, IL',
		department: 'Engineering',
		baseSalary: 7000,
		bonus: 1500,
		totalSalary: 8500,
		status: 'Active'
	},
	{
		id: 4,
		name: 'Sarah Davis',
		phone: '+49 30 12345678',
		email: 'sarah.davis@example.com',
		address: '101 Oak St, Houston, TX',
		department: 'HR',
		baseSalary: 4800,
		bonus: 600,
		totalSalary: 5400,
		status: 'On Leave'
	},
	{
		id: 5,
		name: 'David Wilson',
		phone: '+33 1 2345 6789',
		email: 'd.wilson@example.com',
		address: '202 Pine Rd, Phoenix, AZ',
		department: 'Finance',
		baseSalary: 6500,
		bonus: 1200,
		totalSalary: 7700,
		status: 'Active'
	},
	{
		id: 6,
		name: 'Jennifer Taylor',
		phone: '+81 3 1234 5678',
		email: 'jennifer.t@example.com',
		address: '303 Elm St, Philadelphia, PA',
		department: 'Sales',
		baseSalary: 5200,
		bonus: 900,
		totalSalary: 6100,
		status: 'Active'
	},
	{
		id: 7,
		name: 'Robert Miller',
		phone: '+1 (415) 555-0134',
		email: 'robert.m@example.com',
		address: '404 Cedar Ave, San Antonio, TX',
		department: 'Engineering',
		baseSalary: 7200,
		bonus: 1600,
		totalSalary: 8800,
		status: 'Active'
	},
	{
		id: 8,
		name: 'Lisa Anderson',
		phone: '+44 131 496 0600',
		email: 'lisa.a@example.com',
		address: '505 Maple Dr, San Diego, CA',
		department: 'Marketing',
		baseSalary: 4700,
		bonus: 700,
		totalSalary: 5400,
		status: 'Inactive'
	},
	{
		id: 9,
		name: 'William Thomas',
		phone: '+61 8 8123 4567',
		email: 'william.t@example.com',
		address: '606 Birch Ln, Dallas, TX',
		department: 'Finance',
		baseSalary: 6800,
		bonus: 1300,
		totalSalary: 8100,
		status: 'Active'
	},
	{
		id: 10,
		name: 'Maria Jackson',
		phone: '+49 89 98765432',
		email: 'maria.j@example.com',
		address: '707 Walnut St, San Jose, CA',
		department: 'HR',
		baseSalary: 4900,
		bonus: 650,
		totalSalary: 5550,
		status: 'Active'
	}
])
</script>

Row Selection

Configure row selection via the selection prop. selectedKey controls selected items (controlled mode). If omitted or undefined, the component is uncontrolled; use defaultSelectedKey to provide a default.

Set columns[].disabled to disable the selector for a row.

When there are left-fixed columns, the row selection column is also fixed to the left.

ID
Name
Email
City
Address
1001
Emma Johnson
emma.johnson@example.com
New York
123 Main Street, Apt 4B
1002
James Wilson
j.wilson@example.com
Los Angeles
456 Oak Avenue
1003
Sophia Chen
sophia.chen@example.com
Chicago
789 Pine Road, Suite 12
1004
Michael Brown
m.brown@example.com
Miami
321 Beach Boulevard
1005
Olivia Davis
olivia.davis@example.com
Seattle
654 Lakeview Drive
1
selectedKeys: []
<template>
	<div>
		<px-switch
			style="margin-bottom: 16px"
			active-label="Multiple"
			v-model="selection.multiple"
		></px-switch>
		<px-table
			:data="data"
			:columns="columns"
			row-key="id"
			:selection="selection"
			:scroll="{ x: 1500 }"
			v-model:selected-keys="selectedKeys"
		></px-table>
		<div style="margin-top: 16px">selectedKeys: {{ selectedKeys }}</div>
	</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
	{
		id: 1001,
		name: 'Emma Johnson',
		email: 'emma.johnson@example.com',
		city: 'New York',
		address: '123 Main Street, Apt 4B'
	},
	{
		id: 1002,
		name: 'James Wilson',
		email: 'j.wilson@example.com',
		city: 'Los Angeles',
		address: '456 Oak Avenue',
		disabled: true
	},
	{
		id: 1003,
		name: 'Sophia Chen',
		email: 'sophia.chen@example.com',
		city: 'Chicago',
		address: '789 Pine Road, Suite 12'
	},
	{
		id: 1004,
		name: 'Michael Brown',
		email: 'm.brown@example.com',
		city: 'Miami',
		address: '321 Beach Boulevard'
	},
	{
		id: 1005,
		name: 'Olivia Davis',
		email: 'olivia.davis@example.com',
		city: 'Seattle',
		address: '654 Lakeview Drive'
	}
])

const columns = [
	{
		key: 'id',
		label: 'ID',
		field: 'id',
		fixed: 'left'
	},
	{
		key: 'name',
		label: 'Name',
		field: 'name'
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email'
	},
	{
		key: 'city',
		label: 'City',
		field: 'city'
	},
	{
		key: 'address',
		label: 'Address',
		field: 'address'
	}
]

const selection = ref({
	multiple: false,
	showSelectAll: true
})

const selectedKeys = ref<number[]>([])
</script>

Expandable Rows

Configure expandable rows via the expandable prop. expandedKey controls expanded rows (controlled mode). If omitted or undefined, the component is uncontrolled; use defaultExpandedKey to set a default.

Row expansion content is provided via columns[].expand or the expand slot. If empty (or when the expand slot is set, it is considered false), no expand button is shown.

When there are left-fixed columns, the expand button column is also fixed to the left.

ID
Name
1001
Emma Johnson
1002
James Wilson
1003
Sophia Chen
1004
Michael Brown
1005
Olivia Davis
1
ID
Name
1001
Emma Johnson
1002
James Wilson
1003
Sophia Chen
1004
Michael Brown
1005
Olivia Davis
1
expandedKeys: []
<template>
	<div>
		<px-table
			:data="data0"
			:columns="columns"
			row-key="id"
			expandable
			v-model:expanded-keys="expandedKeys"
		></px-table>
		<px-table
			style="margin-top: 16px"
			:data="data1"
			:columns="columns"
			row-key="id"
			expandable
			v-model:expanded-keys="expandedKeys"
		>
			<template #expand="{ record }">
				<div>
					<div v-for="key in keys" style="display: flex; align-items: center; gap: 8px">
						<div style="color: gray; margin-right: 16px">{{ key }}:</div>
						<div>{{ record[key] }}</div>
					</div>
				</div>
			</template>
		</px-table>
		<div style="margin-top: 16px">expandedKeys: {{ expandedKeys }}</div>
	</div>
</template>

<script setup lang="ts">
import type { TableData } from '@pixelium/web-vue'

// When On-demand Import
// import type { TableData } from '@pixelium/web-vue/es'
import { h, ref } from 'vue'

const keys = ['email', 'city', 'address']
const expandRender = ({ record }: { record: TableData }) => {
	return h(
		'div',
		{},
		keys.map((e) => {
			return h('div', { style: 'display: flex; align-items: center; gap: 8px' }, [
				h('div', { style: 'color: gray; margin-right: 16px' }, e + ': '),
				h('div', {}, record[e])
			])
		})
	)
}
const data0 = ref([
	{
		id: 1001,
		name: 'Emma Johnson',
		email: 'emma.johnson@example.com',
		city: 'New York',
		address: '123 Main Street, Apt 4B',
		expand: expandRender
	},
	{
		id: 1002,
		name: 'James Wilson',
		email: 'j.wilson@example.com',
		city: 'Los Angeles',
		address: '456 Oak Avenue',
		expand: expandRender
	},
	{
		id: 1003,
		name: 'Sophia Chen',
		email: 'sophia.chen@example.com',
		city: 'Chicago',
		address: '789 Pine Road, Suite 12'
	},
	{
		id: 1004,
		name: 'Michael Brown',
		email: 'm.brown@example.com',
		city: 'Miami',
		address: '321 Beach Boulevard',
		expand: expandRender
	},
	{
		id: 1005,
		name: 'Olivia Davis',
		email: 'olivia.davis@example.com',
		city: 'Seattle',
		address: '654 Lakeview Drive',
		expand: expandRender
	}
])
const data1 = ref([
	{
		id: 1001,
		name: 'Emma Johnson',
		email: 'emma.johnson@example.com',
		city: 'New York',
		address: '123 Main Street, Apt 4B'
	},
	{
		id: 1002,
		name: 'James Wilson',
		email: 'j.wilson@example.com',
		city: 'Los Angeles',
		address: '456 Oak Avenue',
		expand: false
	},
	{
		id: 1003,
		name: 'Sophia Chen',
		email: 'sophia.chen@example.com',
		city: 'Chicago',
		address: '789 Pine Road, Suite 12'
	},
	{
		id: 1004,
		name: 'Michael Brown',
		email: 'm.brown@example.com',
		city: 'Miami',
		address: '321 Beach Boulevard'
	},
	{
		id: 1005,
		name: 'Olivia Davis',
		email: 'olivia.davis@example.com',
		city: 'Seattle',
		address: '654 Lakeview Drive'
	}
])

const columns = [
	{
		key: 'id',
		label: 'ID',
		field: 'id',
		fixed: 'left'
	},
	{
		key: 'name',
		label: 'Name',
		field: 'name'
	}
]

const expandedKeys = ref<number[]>([])
</script>

Sorting

Configure sorting using columns[].sortable. sortOrder controls the sorting state of each column (controlled mode). If omitted or undefined, the component is uncontrolled; use defaultSortOrder to provide a default.

The defaultSortOrder property inside columns[].sortable can also set the default for that column.

When sortMethod is set to 'custom', there is no sorting behavior. Backend sorting can be implemented by listening to the sortOrderChange event.

If sortMethod is not provided, the default comparator is used, based on JavaScript's native greater-than/less-than logic.

Name
Age
Email
Emma
31
emma@example.com
James
32
james@example.com
Olivia
28
olivia@example.com
Sophia
24
sophia@example.com
William
29
william@example.com
1
sortOrder: { "name": "asc" }
<template>
	<px-table :data="data" :columns="columns" v-model:sort-order="sortOrder"></px-table>
	<div style="margin-top: 16px">sortOrder: {{ sortOrder }}</div>
</template>

<script setup lang="ts">
import type { SortOrder, TableData } from '@pixelium/web-vue'

// If On-demand Import
// import type { SortOrder, TableData } from '@pixelium/web-vue/es'
import { ref } from 'vue'

const data = ref([
	{ key: 1, name: 'Olivia', age: 28, email: 'olivia@example.com' },
	{ key: 2, name: 'James', age: 32, email: 'james@example.com' },
	{ key: 3, name: 'Sophia', age: 24, email: 'sophia@example.com' },
	{ key: 4, name: 'William', age: 29, email: 'william@example.com' },
	{ key: 5, name: 'Emma', age: 31, email: 'emma@example.com' }
])

const columns = [
	{
		key: 'name',
		label: 'Name',
		field: 'name',
		sortable: {
			orders: ['asc', 'desc'] as const,
			sortMethod: (a: TableData, b: TableData, order: 'asc' | 'desc', field?: string) => {
				const res = a.name.length - b.name.length
				return order === 'desc' ? -res : res
			},
			defaultSortOrder: 'asc' as const
		}
	},
	{
		key: 'age',
		label: 'Age',
		field: 'age',
		sortable: {
			orders: ['asc'] as const
		}
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email',
		sortable: {
			orders: ['asc', 'desc'] as const
		}
	}
]

const sortOrder = ref<SortOrder>({})
</script>

Multi-column Sorting

Enable multi-column sorting by setting multiple: true in columns[].sortable's sortMethod. Use the priority property to set a column's priority; higher values take precedence.

WARNING

Multi-column sorting and single-column sorting are mutually exclusive. Triggering one will clear the selection of the other.

Name
Age
Email
Emma
31
emma@example.com
James
32
james@example.com
Olivia
28
olivia@example.com
Sophia
24
sophia@example.com
William
29
william@example.com
1
sortOrder: { "name": "asc" }
<template>
	<px-table :data="data" :columns="columns" v-model:sort-order="sortOrder"></px-table>
	<div style="margin-top: 16px">sortOrder: {{ sortOrder }}</div>
</template>

<script setup lang="ts">
import type { SortOrder, TableData } from '@pixelium/web-vue'

// If On-demand Import
// import type { SortOrder, TableData } from '@pixelium/web-vue/es'
import { ref } from 'vue'

const data = ref([
	{ key: 1, name: 'Olivia', age: 28, email: 'olivia@example.com' },
	{ key: 2, name: 'James', age: 32, email: 'james@example.com' },
	{ key: 3, name: 'Sophia', age: 24, email: 'sophia@example.com' },
	{ key: 4, name: 'William', age: 29, email: 'william@example.com' },
	{ key: 5, name: 'Emma', age: 31, email: 'emma@example.com' }
])

const columns = [
	{
		key: 'name',
		label: 'Name',
		field: 'name',
		sortable: {
			orders: ['asc', 'desc'] as const,
			sortMethod: (a: TableData, b: TableData, order: 'asc' | 'desc', field?: string) => {
				const res = a.name.length - b.name.length
				return order === 'desc' ? -res : res
			},
			defaultSortOrder: 'asc' as const,
			multiple: true,
			priority: 2
		}
	},
	{
		key: 'age',
		label: 'Age',
		field: 'age',
		sortable: {
			orders: ['asc'] as const,
			multiple: true
		}
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email',
		sortable: {
			orders: ['asc', 'desc'] as const,
			multiple: true,
			priority: 1
		}
	}
]

const sortOrder = ref<SortOrder>({})
</script>

Filtering

Configure filtering with columns[].filterable. filterValue controls the filter state of each column (controlled mode). If omitted or undefined, the component is uncontrolled; use defaultFilterValue to provide a default.

The defaultFilterValue inside columns[].filterable can also set the default for that column.

If filterMethod is not provided, the default comparator is used, based on JavaScript's native === equality.

Name
Base Salary
Bonus
Total
Status
James Wilson
8500
420
8920
active
Michael Brown
11200
150
11350
active
Robert Taylor
9800
920
10820
active
Jennifer Miller
6300
310
6610
active
Lisa Martinez
4500
230
4730
active
Amanda Clark
2900
0
2900
active
1
filterValue: { "status": [ "active" ] }
<template>
	<px-table :data="data" :columns="columns" v-model:filter-value="filterValue"></px-table>
	<div style="margin-top: 16px">filterValue: {{ filterValue }}</div>
</template>

<script setup lang="ts">
import type { FilterValue, TableData } from '@pixelium/web-vue'

// If On-demand Import
// import type { FilterValue, TableData } from '@pixelium/web-vue/es'
import { ref } from 'vue'

const columns = [
	{
		label: 'Name',
		field: 'name',
		key: 'name'
	},
	{
		label: 'Base Salary',
		field: 'baseSalary',
		key: 'baseSalary',
		filterable: {
			filterOptions: [{ label: '> 7000', value: 7000 }],
			filterMethod: (value: number[], record: TableData, field?: string) => {
				return value[0] ? record.baseSalary > value[0] : true
			}
		}
	},
	{
		label: 'Bonus',
		field: 'bonus',
		key: 'bonus',
		filterable: {
			filterOptions: [{ label: '> 500', value: 500 }],
			filterMethod: (value: number[], record: TableData, field?: string) => {
				return value[0] ? record.bonus > value[0] : true
			}
		}
	},
	{
		label: 'Total',
		field: 'total',
		key: 'total',
		filterable: {
			filterOptions: [{ label: '> 10000', value: 500 }],
			filterMethod: (value: number[], record: TableData, field?: string) => {
				return value[0] ? record.total > value[0] : true
			}
		}
	},
	{
		label: 'Status',
		field: 'status',
		key: 'status',
		filterable: {
			multiple: true,
			filterOptions: [
				{ label: 'Active', value: 'active' },
				{ label: 'Inactive', value: 'inactive' }
			],
			defaultFilterValue: ['active']
		}
	}
]

const data = ref([
	{
		name: 'James Wilson',
		baseSalary: 8500,
		bonus: 420,
		total: 8920,
		status: 'active'
	},
	{
		name: 'Sarah Johnson',
		baseSalary: 5200,
		bonus: 780,
		total: 5900,
		status: 'inactive'
	},
	{
		name: 'Michael Brown',
		baseSalary: 11200,
		bonus: 150,
		total: 11350,
		status: 'active'
	},
	{
		name: 'Emily Davis',
		baseSalary: 3400,
		bonus: 0,
		total: 3400,
		status: 'inactive'
	},
	{
		name: 'Robert Taylor',
		baseSalary: 9800,
		bonus: 920,
		total: 10820,
		status: 'active'
	},
	{
		name: 'Jennifer Miller',
		baseSalary: 6300,
		bonus: 310,
		total: 6610,
		status: 'active'
	},
	{
		name: 'David Anderson',
		baseSalary: 7600,
		bonus: 650,
		total: 8250,
		status: 'inactive'
	},
	{
		name: 'Lisa Martinez',
		baseSalary: 4500,
		bonus: 230,
		total: 4730,
		status: 'active'
	},
	{
		name: 'William Thomas',
		baseSalary: 10500,
		bonus: 870,
		total: 11370,
		status: 'inactive'
	},
	{
		name: 'Amanda Clark',
		baseSalary: 2900,
		bonus: 0,
		total: 2900,
		status: 'active'
	}
])

const filterValue = ref<FilterValue>({})
</script>

Summary Row

Configure summary rows with the summary prop.

  • summary.data sets the content of the summary row.
  • summary.summaryText sets the text for the first column of the summary row.
  • summary.placement adjusts the placement of the summary row; choose 'start' or 'end' (default) to place the summary at the top or bottom of the data area.
  • summary.fixed configures whether the summary row is fixed; the summary row is fixed by default.
  • summary.spanMethod configures cell merging within the summary row area.

WARNING

When the summary row is placed at the start of the data area, a fixed summary row requires a fixed header to take effect.

Placement:
Name
Base Salary
Bonus
Total
Status
Emma Johnson
8500
420
8920
active
David Smith
6500
780
7280
inactive
Sophia Williams
11200
150
11350
active
Michael Brown
4200
0
4200
inactive
Olivia Davis
9800
920
10720
active
James Wilson
7300
310
7610
active
Sarah Miller
10500
850
11350
inactive
Robert Taylor
4200
190
4390
active
Jennifer Anderson
8900
0
8900
inactive
Thomas Clark
11500
970
12470
active
Sum
82600
4590
87190
Average
8260
459
8719
1
<template>
	<div style="display: flex; align-items: center">
		<px-switch
			active-label="Fixed"
			inactive-label="Unfixed"
			v-model="summaryFixed"
			style="margin-right: 16px"
		></px-switch>
		Placement:
		<px-switch
			active-label="Start"
			inactive-label="End"
			v-model="summaryStart"
			style="margin-left: 8px"
		></px-switch>
	</div>
	<px-table
		:data="data"
		:columns="columns"
		:summary="summary"
		style="margin-top: 16px"
		:table-area-props="{ style: 'max-height: 300px' }"
	></px-table>
</template>

<script setup lang="ts">
import type { TableOptionsArg } from '@pixelium/web-vue'
// If on-demand import
// import type { TableOptionsArg } from '@pixelium/web-vue/es'

import { computed, ref } from 'vue'

const columns = [
	{
		label: 'Name',
		field: 'name',
		key: 'name'
	},
	{
		label: 'Base Salary',
		field: 'baseSalary',
		key: 'baseSalary'
	},
	{
		label: 'Bonus',
		field: 'bonus',
		key: 'bonus'
	},
	{
		label: 'Total',
		field: 'total',
		key: 'total'
	},
	{
		label: 'Status',
		field: 'status',
		key: 'status'
	}
]

const data = ref([
	{
		name: 'Emma Johnson',
		baseSalary: 8500,
		bonus: 420,
		total: 8920,
		status: 'active'
	},
	{
		name: 'David Smith',
		baseSalary: 6500,
		bonus: 780,
		total: 7280,
		status: 'inactive'
	},
	{
		name: 'Sophia Williams',
		baseSalary: 11200,
		bonus: 150,
		total: 11350,
		status: 'active'
	},
	{
		name: 'Michael Brown',
		baseSalary: 4200,
		bonus: 0,
		total: 4200,
		status: 'inactive'
	},
	{
		name: 'Olivia Davis',
		baseSalary: 9800,
		bonus: 920,
		total: 10720,
		status: 'active'
	},
	{
		name: 'James Wilson',
		baseSalary: 7300,
		bonus: 310,
		total: 7610,
		status: 'active'
	},
	{
		name: 'Sarah Miller',
		baseSalary: 10500,
		bonus: 850,
		total: 11350,
		status: 'inactive'
	},
	{
		name: 'Robert Taylor',
		baseSalary: 4200,
		bonus: 190,
		total: 4390,
		status: 'active'
	},
	{
		name: 'Jennifer Anderson',
		baseSalary: 8900,
		bonus: 0,
		total: 8900,
		status: 'inactive'
	},
	{
		name: 'Thomas Clark',
		baseSalary: 11500,
		bonus: 970,
		total: 12470,
		status: 'active'
	}
])

const summaryData = [
	{
		baseSalary: 0,
		bonus: 0,
		total: 0
	},
	{
		baseSalary: 0,
		bonus: 0,
		total: 0
	}
]
data.value.forEach((record) => {
	summaryData[0].baseSalary += record.baseSalary
	summaryData[0].bonus += record.bonus
	summaryData[0].total += record.total
})
summaryData[1].baseSalary = parseFloat(
	(summaryData[0].baseSalary / data.value.length).toFixed(4)
)
summaryData[1].bonus = parseFloat((summaryData[0].bonus / data.value.length).toFixed(4))
summaryData[1].total = parseFloat((summaryData[0].total / data.value.length).toFixed(4))

const summaryFixed = ref(false)
const summaryStart = ref(false)
const summary = computed(() => {
	return {
		data: summaryData,
		summaryText: ['Sum', 'Average'],
		fixed: summaryFixed.value,
		placement: summaryStart.value ? 'start' : 'end',
		spanMethod: ({ rowIndex, colIndex, record, column }: TableOptionsArg) => {
			if (colIndex === 3) {
				return {
					colspan: 2
				}
			}
		}
	}
})
</script>

Loading State

Configure the loading state of the table by setting the loading property.

Name
Age
Email
Alice
25
alice@example.com
Bob
30
bob@example.com
Charlie
35
charlie@example.com
1
<template>
	<px-switch active-tip="loading" inactive-tip="not loading" v-model="loading"></px-switch>
	<px-table
		:data="data"
		:columns="columns"
		:loading="loading"
		style="margin-top: 16px"
	></px-table>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const loading = ref(false)

const data = ref([
	{ key: 1, name: 'Alice', age: 25, email: 'alice@example.com' },
	{ key: 2, name: 'Bob', age: 30, email: 'bob@example.com' },
	{ key: 3, name: 'Charlie', age: 35, email: 'charlie@example.com' }
])

const columns = [
	{
		key: 'name',
		label: 'Name',
		field: 'name'
	},
	{
		key: 'age',
		label: 'Age',
		field: 'age'
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email'
	}
]
</script>

Client-side Pagination

The table component supports automatic pagination of the passed data by default. The pagination configuration can be set via the pagination property.

ID
User
Email
Register
1
Lisa Jackson
lisa.jackson@example.com
2025-04-06
2
Stephanie Thompson
stephanie.thompson@example.com
2026-01-05
3
Michael Taylor
michael.taylor@example.com
2025-08-13
4
William Wilson
william.wilson@example.com
2025-10-16
5
Michael Anderson
michael.anderson@example.com
2025-08-01
6
Christopher Miller
christopher.miller@example.com
2026-01-11
7
Christopher Thomas
christopher.thomas@example.com
2025-09-14
8
James Jones
james.jones@example.com
2025-06-25
9
Emma Brown
emma.brown@example.com
2025-07-15
10
Christopher Harris
christopher.harris@example.com
2025-10-19
Total 50
1
2
3
4
5
10 / Page
Go to
page: 1; pageSize: 10
<template>
	<px-table
		:data="data"
		:columns="columns"
		:pagination="{
			showTotal: true,
			showJumper: true,
			showSize: true
		}"
		v-model:page-size="pageSize"
		v-model:page="page"
	></px-table>
	<div style="margin-top: 16px">page: {{ page }}; pageSize: {{ pageSize }}</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'

const pageSize = ref(10)
const page = ref(1)

const columns = [
	{
		key: 'id',
		label: 'ID',
		field: 'id'
	},
	{
		key: 'user',
		label: 'User',
		field: 'user'
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email'
	},
	{
		key: 'register',
		label: 'Register',
		field: 'register'
	}
]

const firstNames = [
	'John',
	'Emma',
	'Michael',
	'Sarah',
	'David',
	'Lisa',
	'Robert',
	'Jennifer',
	'William',
	'Jessica',
	'James',
	'Amanda',
	'Christopher',
	'Melissa',
	'Daniel',
	'Stephanie',
	'Matthew',
	'Laura',
	'Joshua',
	'Michelle'
]

const lastNames = [
	'Smith',
	'Johnson',
	'Williams',
	'Brown',
	'Jones',
	'Miller',
	'Davis',
	'Garcia',
	'Rodriguez',
	'Wilson',
	'Martinez',
	'Anderson',
	'Taylor',
	'Thomas',
	'Jackson',
	'White',
	'Harris',
	'Martin',
	'Thompson',
	'Moore'
]

function generateRandomData(count: number) {
	const data: any[] = []
	const currentDate = new Date()

	for (let i = 1; i <= count; i++) {
		const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]
		const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]
		const fullName = `${firstName} ${lastName}`

		const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`

		const randomDaysAgo = Math.floor(Math.random() * 365)
		const registerDate = new Date(currentDate)
		registerDate.setDate(registerDate.getDate() - randomDaysAgo)
		const registerDateStr = registerDate.toISOString().split('T')[0]

		data.push({
			id: i,
			user: fullName,
			email: email,
			register: registerDateStr
		})
	}

	return data
}

const data = ref(generateRandomData(50))
</script>

Server-side Pagination

When the pagination.paginateMethod is set to 'custom', the table will not perform automatic sorting. This is often used for server-side pagination. The total number of data items can be set via pagination.total.

ID
User
Email
Register
1
David Thomas
david.thomas@example.com
2025-11-07
2
Stephanie Jackson
stephanie.jackson@example.com
2025-07-20
3
Matthew Martinez
matthew.martinez@example.com
2025-11-12
4
James Thomas
james.thomas@example.com
2025-07-29
5
Matthew White
matthew.white@example.com
2025-10-21
6
Jennifer Rodriguez
jennifer.rodriguez@example.com
2025-12-10
7
Christopher Garcia
christopher.garcia@example.com
2026-02-10
8
William Martinez
william.martinez@example.com
2026-01-19
9
Lisa Anderson
lisa.anderson@example.com
2025-04-21
10
Christopher Smith
christopher.smith@example.com
2025-04-24
Total 1000
1
2
3
4
5
6
7
...
100
10 / Page
Go to
page: 1; pageSize: 10
<template>
	<px-table
		:data="data"
		:columns="columns"
		:pagination="{
			showTotal: true,
			showJumper: true,
			showSize: true,
			total: 1000,
			paginateMethod: 'custom'
		}"
		v-model:page-size="pageSize"
		v-model:page="page"
		:loading="loading"
	></px-table>
	<div style="margin-top: 16px">page: {{ page }}; pageSize: {{ pageSize }}</div>
</template>

<script lang="ts" setup>
import { ref, watch } from 'vue'

const pageSize = ref(10)
const page = ref(1)

const loading = ref(false)

watch([page, pageSize], () => {
	loading.value = true
	setTimeout(() => {
		data.value = generateRandomData(pageSize.value, pageSize.value * (page.value - 1))
		loading.value = false
	}, 3000)
})

const columns = [
	{
		key: 'id',
		label: 'ID',
		field: 'id'
	},
	{
		key: 'user',
		label: 'User',
		field: 'user'
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email'
	},
	{
		key: 'register',
		label: 'Register',
		field: 'register'
	}
]

const firstNames = [
	'John',
	'Emma',
	'Michael',
	'Sarah',
	'David',
	'Lisa',
	'Robert',
	'Jennifer',
	'William',
	'Jessica',
	'James',
	'Amanda',
	'Christopher',
	'Melissa',
	'Daniel',
	'Stephanie',
	'Matthew',
	'Laura',
	'Joshua',
	'Michelle'
]

const lastNames = [
	'Smith',
	'Johnson',
	'Williams',
	'Brown',
	'Jones',
	'Miller',
	'Davis',
	'Garcia',
	'Rodriguez',
	'Wilson',
	'Martinez',
	'Anderson',
	'Taylor',
	'Thomas',
	'Jackson',
	'White',
	'Harris',
	'Martin',
	'Thompson',
	'Moore'
]

function generateRandomData(count: number, startId: number) {
	const data: any[] = []
	const currentDate = new Date()

	for (let i = 1; i <= count; i++) {
		const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]
		const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]
		const fullName = `${firstName} ${lastName}`

		const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`

		const randomDaysAgo = Math.floor(Math.random() * 365)
		const registerDate = new Date(currentDate)
		registerDate.setDate(registerDate.getDate() - randomDaysAgo)
		const registerDateStr = registerDate.toISOString().split('T')[0]

		data.push({
			id: i + startId,
			user: fullName,
			email: email,
			register: registerDateStr
		})
	}

	return data
}

const data = ref(generateRandomData(pageSize.value, pageSize.value * (page.value - 1)))
</script>

Hide Pagination on Single Page

Set pagination.hideWhenSinglePage to hide pagination when there is only a single page.

Name
Age
Email
Alice
25
alice@example.com
Bob
30
bob@example.com
Charlie
35
charlie@example.com
<template>
	<px-table
		:data="data"
		:columns="columns"
		:pagination="{ hideWhenSinglePage: true }"
	></px-table>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const data = ref([
	{ key: 1, name: 'Alice', age: 25, email: 'alice@example.com' },
	{ key: 2, name: 'Bob', age: 30, email: 'bob@example.com' },
	{ key: 3, name: 'Charlie', age: 35, email: 'charlie@example.com' }
])

const columns = [
	{
		key: 'name',
		label: 'Name',
		field: 'name'
	},
	{
		key: 'age',
		label: 'Age',
		field: 'age'
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email'
	}
]
</script>

Instance Methods

It is important to note that, for the purpose of state synchronization in controlled mode, instance methods such as selection, expansion, filtering, and sorting are designed asynchronously. Each invocation may require waiting for a microtask delay to take effect.

expandedKeys: []
selectedKeys: []
sortOrder: {}
filterValue: {}
ID
Name
1001
Harper Brown
1002
Sophia Thomas
1003
Noah Wilson
1004
William Miller
1005
Charlotte Thomas
1006
James Rodriguez
1007
Sophia Walker
1008
Ava Moore
1009
James Miller
1010
Olivia Walker
1
2
3
4
5
<template>
	<div>
		<px-space>
			<px-button @click="logCurrentData" theme="info">Log Current Data</px-button>
			<px-button @click="logPaginatedData" theme="info">Log Paginated Data</px-button>
		</px-space>
		<px-space style="margin-top: 16px">
			<px-button @click="expand1stRow">Expand 1st Row</px-button>
			<px-button @click="clearExpand" theme="warning">Clear Expand</px-button>
		</px-space>
		<px-space style="margin-top: 16px">
			<px-button @click="select2ndRow">Select 2nd Row</px-button>
			<px-button @click="clearSelect" theme="warning">Clear Select</px-button>
			<px-button @click="selectAll">Select All</px-button>
		</px-space>
		<px-space style="margin-top: 16px">
			<px-button @click="sort1stCol">Sort 1st Col</px-button>
			<px-button @click="clearSort" theme="warning">Clear Sort</px-button>
			<px-button @click="filter2stCol">FIlter 2st Col</px-button>
			<px-button @click="clearFilter" theme="warning">Clear Filter</px-button>
		</px-space>
		<div style="margin-top: 16px">expandedKeys: {{ expandedKeys }}</div>
		<div style="margin-top: 16px">selectedKeys: {{ selectedKeys }}</div>
		<div style="margin-top: 16px">sortOrder: {{ sortOrder }}</div>
		<div style="margin-top: 16px">filterValue: {{ filterValue }}</div>
		<px-table
			style="margin-top: 16px"
			:data="data"
			:columns="columns"
			row-key="id"
			expandable
			v-model:expanded-keys="selectedKeys"
			v-model:selected-keys="expandedKeys"
			v-model:sort-order="sortOrder"
			v-model:filter-value="filterValue"
			:selection="{
				multiple: true,
				showSelectAll: true
			}"
			ref="tableRef"
		></px-table>
	</div>
</template>

<script setup lang="ts">
import type { TableData, Table, SortOrder, FilterValue, TableColumn } from '@pixelium/web-vue'

// When On-demand Import
// import type { TableData, Table, SortOrder, FilterValue } from '@pixelium/web-vue/es'
import { h, ref } from 'vue'

const tableRef = ref<null | InstanceType<typeof Table>>(null)

const logCurrentData = () => {
	console.log(tableRef.value?.getCurrentData())
}
const logPaginatedData = () => {
	console.log(tableRef.value?.getPaginatedData())
}

const filter2stCol = () => {
	tableRef.value?.filter('name', ['C'])
}
const clearFilter = () => {
	tableRef.value?.clearFilter()
}

const sort1stCol = () => {
	tableRef.value?.sort('id', 'desc')
}
const clearSort = () => {
	tableRef.value?.clearSort()
}

const select2ndRow = () => {
	tableRef.value?.select(1002, true)
}
const clearSelect = () => {
	tableRef.value?.clearSelect()
}
const selectAll = () => {
	tableRef.value?.selectAll(true)
}

const expand1stRow = () => {
	tableRef.value?.expand(1001, true)
}
const clearExpand = () => {
	tableRef.value?.clearExpand()
}

const keys = ['email', 'city', 'address']
const expandRender = ({ record }: { record: TableData }) => {
	return h(
		'div',
		{},
		keys.map((e) => {
			return h('div', { style: 'display: flex; align-items: center; gap: 8px' }, [
				h('div', { style: 'color: gray; margin-right: 16px' }, e + ': '),
				h('div', {}, record[e])
			])
		})
	)
}

function generateData(count: number, startId: number = 1001) {
	const firstNames = [
		'Emma',
		'James',
		'Sophia',
		'Michael',
		'Olivia',
		'Liam',
		'Ava',
		'Noah',
		'Isabella',
		'William',
		'Mia',
		'Ethan',
		'Charlotte',
		'Alexander',
		'Amelia',
		'Benjamin',
		'Harper',
		'Daniel',
		'Evelyn',
		'Matthew'
	]
	const lastNames = [
		'Johnson',
		'Wilson',
		'Chen',
		'Brown',
		'Davis',
		'Miller',
		'Garcia',
		'Rodriguez',
		'Martinez',
		'Taylor',
		'Anderson',
		'Thomas',
		'Jackson',
		'White',
		'Harris',
		'Martin',
		'Thompson',
		'Moore',
		'Walker',
		'Clark'
	]
	const cities = [
		'New York',
		'Los Angeles',
		'Chicago',
		'Miami',
		'Seattle',
		'Houston',
		'Phoenix',
		'Philadelphia',
		'San Antonio',
		'San Diego',
		'Dallas',
		'San Jose',
		'Austin',
		'Jacksonville',
		'Fort Worth',
		'Columbus',
		'Charlotte',
		'San Francisco',
		'Indianapolis',
		'Denver'
	]
	const streetNames = [
		'Main Street',
		'Oak Avenue',
		'Pine Road',
		'Beach Boulevard',
		'Lakeview Drive',
		'Maple Lane',
		'Cedar Street',
		'Elm Avenue',
		'Hill Road',
		'Park Boulevard',
		'River Drive',
		'Sunset Avenue',
		'Mountain Road',
		'Valley Drive',
		'Ocean Boulevard',
		'Forest Lane',
		'Spring Avenue',
		'Summer Road',
		'Winter Drive',
		'Autumn Lane'
	]

	const data = []

	for (let i = 0; i < count; i++) {
		const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]
		const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]
		const city = cities[Math.floor(Math.random() * cities.length)]
		const streetName = streetNames[Math.floor(Math.random() * streetNames.length)]
		const streetNumber = Math.floor(Math.random() * 900) + 100

		const aptTypes = ['Apt', 'Suite', 'Unit', '#', null]
		const aptType = aptTypes[Math.floor(Math.random() * aptTypes.length)]
		const aptNumber = aptType
			? Math.floor(Math.random() * 20) +
				1 +
				(Math.random() > 0.5 ? String.fromCharCode(65 + Math.floor(Math.random() * 5)) : '')
			: ''

		let address = `${streetNumber} ${streetName}`
		if (aptType && aptNumber) {
			address += `, ${aptType} ${aptNumber}`
		}

		const item = {
			id: startId + i,
			name: `${firstName} ${lastName}`,
			email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`,
			city: city,
			address: address,
			expand: expandRender
		}

		data.push(item)
	}

	return data
}

const data = ref(generateData(50))

const columns: TableColumn[] = [
	{
		key: 'id',
		label: 'ID',
		field: 'id',
		fixed: 'left',
		sortable: {
			orders: ['asc', 'desc']
		}
	},
	{
		key: 'name',
		label: 'Name',
		field: 'name',
		filterable: {
			filterOptions: Array.from({ length: 26 }, (_, i) => ({
				label: `${String.fromCharCode(65 + i)}...`,
				value: String.fromCharCode(65 + i)
			})),
			filterMethod: (value: string[], record: TableData) => {
				if (value.length) {
					return record.name[0] === value[0]
				} else {
					return true
				}
			}
		}
	}
]

const expandedKeys = ref<number[]>([])
const selectedKeys = ref<number[]>([])
const sortOrder = ref<SortOrder>({})
const filterValue = ref<FilterValue>({})
</script>

Cross-Page Select All

selection.selectAllMethod can asynchronously customize the selectedKeys value updated during a select-all operation.

selection.supersetSelectAllRef configures the superset referenced for the select-all checkbox's checked status: 'current' (data on the current page, default) or 'all' (data across all pages).

Superset for Select All State Reference
ID
User
Email
Register
1
David Miller
david.miller@example.com
2025-04-12
2
Robert Wilson
robert.wilson@example.com
2025-11-20
3
Laura Taylor
laura.taylor@example.com
2025-12-09
4
Emma Smith
emma.smith@example.com
2025-08-30
5
Matthew Wilson
matthew.wilson@example.com
2025-06-03
6
Christopher Garcia
christopher.garcia@example.com
2025-04-23
7
Laura White
laura.white@example.com
2025-10-02
8
Michael Harris
michael.harris@example.com
2025-06-13
9
Robert Garcia
robert.garcia@example.com
2025-07-19
10
Sarah Williams
sarah.williams@example.com
2025-08-23
Total 50
1
2
3
4
5
10 / Page
<template>
	<div style="display: flex; align-items: center">
		Superset for Select All State Reference
		<px-switch
			style="margin-left: 16px"
			v-model="supersetWasAllData"
			active-label="All Data"
		></px-switch>
	</div>
	<px-table
		style="margin-top: 16px"
		:data="data"
		rowKey="id"
		:columns="columns"
		:pagination="{
			showTotal: true,
			showSize: true
		}"
		v-model:page-size="pageSize"
		v-model:page="page"
		:selection="{
			selectAllMethod,
			supersetSelectAllRef,
			multiple: true
		}"
		v-model:selected-keys="selectedKeys"
		ref="tableRef"
	>
		<template #footer>
			<div
				v-if="!crossPageSelectAll && currentPageSelectAll"
				style="color: var(--px-neutral-8)"
			>
				Select all on current page
			</div>
			<div v-if="crossPageSelectAll" style="color: var(--px-primary-6)">
				Select all across all pages
			</div>
		</template>
	</px-table>
</template>

<script lang="ts" setup>
import type { DialogReturn, TableData } from '@pixelium/web-vue'
import { Button, Dialog, Table } from '@pixelium/web-vue'

// On-demand import
// import type { DialogReturn, TableData } from '@pixelium/web-vue/es'
// import { Button, Dialog, Table } from '@pixelium/web-vue/es'

import { ref, h, computed, watch, nextTick } from 'vue'

function intersection<T>(arr1: T[], arr2: T[]): T[] {
	const map = new Map<any, { value: T; count: number }>()
	const len1 = arr1.length
	for (let i = 0; i < len1; i++) {
		const key = arr1[i]
		const gotVal = map.get(key)
		if (!gotVal) {
			map.set(key, { value: arr1[i], count: 1 })
		} else {
			gotVal.count += 1
		}
	}
	const len2 = arr2.length
	for (let i = 0; i < len2; i++) {
		const key = arr2[i]
		const gotVal = map.get(key)
		if (!gotVal) {
			map.set(key, { value: arr2[i], count: 1 })
		} else {
			gotVal.count += 1
		}
	}
	const ans: T[] = []
	for (const data of map.values()) {
		if (data.count > 1) {
			ans.push(data.value)
		}
	}
	return ans
}

function union<T>(arr1: T[], arr2: T[]): T[] {
	const map = new Map<any, T>()
	const len1 = arr1.length
	for (let i = 0; i < len1; i++) {
		const key = arr1[i]
		if (!map.has(key)) {
			map.set(key, arr1[i])
		}
	}
	const len2 = arr2.length
	for (let i = 0; i < len2; i++) {
		const key = arr2[i]
		if (!map.has(key)) {
			map.set(key, arr2[i])
		}
	}
	return [...map.values()]
}

function difference<T>(arr1: T[], arr2: T[]): T[] {
	const map = new Map<any, { value: T; count: number }>()
	const len1 = arr1.length
	for (let i = 0; i < len1; i++) {
		const key = arr1[i]
		if (!map.has(key)) {
			map.set(key, { value: arr1[i], count: 1 })
		}
	}
	const len2 = arr2.length
	for (let i = 0; i < len2; i++) {
		const key = arr2[i]
		const gotVal = map.get(key)
		if (gotVal) {
			gotVal.count++
		}
	}
	const ans: T[] = []
	for (const data of map.values()) {
		if (data.count === 1) {
			ans.push(data.value)
		}
	}
	return ans
}

const getKeys = (data: TableData[]) => data.filter((e) => !e.disabled).map((e) => e.id)

const pageSize = ref(10)
const page = ref(1)

const supersetWasAllData = ref(false)
const supersetSelectAllRef = computed(() => {
	return supersetWasAllData.value ? 'all' : 'current'
})

const tableRef = ref<InstanceType<typeof Table> | null>(null)
const selectedKeys = ref<number[]>([])
const currentPageSelectAll = ref(false)
const crossPageSelectAll = ref(false)
watch([page, pageSize, selectedKeys], () => {
	nextTick(() => {
		const currentData = tableRef.value?.getCurrentData() || []
		const paginatedData = tableRef.value?.getPaginatedData() || []
		currentPageSelectAll.value =
			difference(getKeys(paginatedData), selectedKeys.value).length === 0
		crossPageSelectAll.value = difference(getKeys(currentData), selectedKeys.value).length === 0
	})
})

const selectAllMethod = async (
	value: boolean,
	preState: { value: boolean; indeterminate: boolean },
	extra: {
		originData: TableData[]
		currentData: TableData[]
		paginatedData: TableData[]
		selectedKeys: any[]
		page: number
		pageSize: number
	}
) => {
	const paginatedDataKeys = getKeys(extra.paginatedData)
	const currentDataKeys = getKeys(extra.currentData)
	let selectedKeys: any[] = []
	const clearPageHandler = (dialogReturn: DialogReturn) => {
		dialogReturn.close()
		selectedKeys = difference(extra.selectedKeys, paginatedDataKeys)
	}
	const pageOnlyHandler = (dialogReturn: DialogReturn) => {
		dialogReturn.close()
		selectedKeys = getKeys(extra.paginatedData)
	}
	const pageAddHandler = (dialogReturn: DialogReturn) => {
		dialogReturn.close()
		selectedKeys = union(extra.selectedKeys, paginatedDataKeys)
	}
	const selectAllHandler = (dialogReturn: DialogReturn) => {
		dialogReturn.close()
		selectedKeys = union(extra.selectedKeys, currentDataKeys)
	}
	const clearAllHandler = (dialogReturn: DialogReturn) => {
		dialogReturn.close()
		selectedKeys = difference(extra.selectedKeys, currentDataKeys)
	}
	let dialogReturn: DialogReturn
	if (value) {
		const currentPageHasIntersection =
			extra.selectedKeys.length &&
			intersection(paginatedDataKeys, extra.selectedKeys).length > 0
		const allDataHasIntersection =
			supersetWasAllData.value &&
			extra.selectedKeys.length &&
			intersection(currentDataKeys, extra.selectedKeys).length > 0
		dialogReturn = Dialog.warning({
			title: 'Select All',
			content: 'Select one action to perform:',
			footer: () => {
				return h('div', {}, [
					currentPageHasIntersection && allDataHasIntersection
						? h(
								'div',
								{
									style:
										'display: flex; justify-content: end; align-items: center; gap: 8px; margin-bottom: 8px'
								},
								{
									default: () => [
										currentPageHasIntersection
											? h(
													Button,
													{
														theme: 'info',
														onClick: () => clearPageHandler(dialogReturn),
														pollSizeChange: true
													},
													{ default: () => 'Clear Page' }
												)
											: null,
										allDataHasIntersection
											? h(
													Button,
													{
														theme: 'warning',
														onClick: () => clearAllHandler(dialogReturn),
														pollSizeChange: true
													},
													{ default: () => 'Clear All' }
												)
											: null
									]
								}
							)
						: null,
					h(
						'div',
						{ style: 'display: flex; justify-content: end; align-items: center; gap: 8px;' },
						{
							default: () => [
								h(
									Button,
									{
										theme: 'info',
										onClick: () => pageOnlyHandler(dialogReturn),
										pollSizeChange: true
									},
									{ default: () => 'This Page Only' }
								),
								h(
									Button,
									{
										theme: 'primary',
										onClick: () => pageAddHandler(dialogReturn),
										pollSizeChange: true
									},
									{ default: () => 'Add This Page' }
								),
								h(
									Button,
									{
										theme: 'primary',
										onClick: () => selectAllHandler(dialogReturn),
										pollSizeChange: true
									},
									{ default: () => 'Select All' }
								)
							]
						}
					)
				])
			}
		})
	} else {
		dialogReturn = Dialog.warning({
			title: 'Clear Select All',
			content: 'Select one action to perform:',
			footer: () => {
				return h(
					'div',
					{ style: 'display: flex; justify-content: end; align-items: center; gap: 8px' },
					{
						default: () => [
							h(
								Button,
								{
									theme: 'info',
									onClick: () => clearPageHandler(dialogReturn),
									pollSizeChange: true
								},
								{ default: () => 'Clear Page' }
							),
							h(
								Button,
								{
									theme: 'warning',
									onClick: () => clearAllHandler(dialogReturn),
									pollSizeChange: true
								},
								{ default: () => 'Clear All' }
							)
						]
					}
				)
			}
		})
	}
	await dialogReturn
	return selectedKeys
}

const columns = [
	{
		key: 'id',
		label: 'ID',
		field: 'id'
	},
	{
		key: 'user',
		label: 'User',
		field: 'user'
	},
	{
		key: 'email',
		label: 'Email',
		field: 'email'
	},
	{
		key: 'register',
		label: 'Register',
		field: 'register'
	}
]

const firstNames = [
	'John',
	'Emma',
	'Michael',
	'Sarah',
	'David',
	'Lisa',
	'Robert',
	'Jennifer',
	'William',
	'Jessica',
	'James',
	'Amanda',
	'Christopher',
	'Melissa',
	'Daniel',
	'Stephanie',
	'Matthew',
	'Laura',
	'Joshua',
	'Michelle'
]

const lastNames = [
	'Smith',
	'Johnson',
	'Williams',
	'Brown',
	'Jones',
	'Miller',
	'Davis',
	'Garcia',
	'Rodriguez',
	'Wilson',
	'Martinez',
	'Anderson',
	'Taylor',
	'Thomas',
	'Jackson',
	'White',
	'Harris',
	'Martin',
	'Thompson',
	'Moore'
]

function generateRandomData(count: number) {
	const data: any[] = []
	const currentDate = new Date()

	for (let i = 1; i <= count; i++) {
		const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]
		const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]
		const fullName = `${firstName} ${lastName}`

		const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`

		const randomDaysAgo = Math.floor(Math.random() * 365)
		const registerDate = new Date(currentDate)
		registerDate.setDate(registerDate.getDate() - randomDaysAgo)
		const registerDateStr = registerDate.toISOString().split('T')[0]

		data.push({
			id: i,
			user: fullName,
			email: email,
			register: registerDateStr
		})
	}

	return data
}

const data = ref(generateRandomData(50))
</script>

API

TableProps

AttributeTypeOptionalDefaultDescriptionVersion
dataTableData[]True[]The table's data source.0.1.0
columnsTableColumn[]True[]The table's column configuration collection.0.1.0
borderedboolean | TableBorderedTruetrueWhether to display table borders and related styles.0.1.0
variant'normal' | 'striped' | 'checkered'True'normal'The table's display variant/style.0.1.0
fixedHeadbooleanTruetrueWhether the header is fixed.0.1.0
spanMethod(options: TableOptionsArg) => void | { colspan?: number, rowspan?: number}TrueThe method used to merge rows or columns.0.1.0
rowKeystringTrue'key'The key name used to uniquely identify row data.0.1.0
scroll{ x?: number | string }TrueTable scroll configuration (e.g., horizontal scroll settings).0.1.0
selectionboolean | TableSelectionTruefalseWhether row selection is enabled and its configuration.0.1.0
selectedKeysany[] | nullTrueCurrently selected row keys (controlled).0.1.0
defaultSelectedKeysany[] | nullTrueDefault selected row keys (uncontrolled).0.1.0
expandableboolean | TableExpandableTruefalseWhether expandable rows are enabled and its configuration.0.1.0
expandedKeysany[] | nullTrueCurrently expanded row keys (controlled).0.1.0
defaultExpandedKeysany[] | nullTrueDefault expanded row keys (uncontrolled).0.1.0
summaryTableSummaryTrueConfiguration for the table's summary row.0.1.0
filterValueFilterValue | nullTrueCurrent filter values (controlled).0.1.0
defaultFilterValueFilterValue | nullTrueDefault filter values (uncontrolled).0.1.0
sortOrderSortOrder | nullTrueCurrent sorting information (controlled).0.1.0
defaultSortOrderSortOrder | nullTrueDefault sorting information (uncontrolled).0.1.0
loadingbooleanTruefalseWhether table is loading.0.1.0
paginationTablePaginationTruetrueConfiguration for table pagination.0.1.0
pagenumber | nullTrueCurrent page number (controlled mode).0.1.0
defaultPagenumber | nullTrue1Default value of current page number (uncontrolled mode).0.1.0
pageSizenumber | nullTrueCurrent page size (controlled mode).0.1.0
defaultPageSizenumber | nullTrue10Default value of current page size (uncontrolled mode).0.1.0
tableAreaPropsRestAttrsTrueA table area property object. The element it acts on can be found via .px-table-area.0.1.0
borderRadiusnumberTrueTable border-radius settings.0.1.0
pollSizeChangebooleanTrueEnable polling for component size changes. This may affect performance and is commonly used when a container element affects the component's size, causing canvas rendering issues. This prop also applies to internal Radio, Checkbox and Pagination child components.0.1.0

TableEvents

EventParameterDescriptionVersion
update:selectedKeyvalue: any[]Callback when selectedKeys is updated.0.1.0
selectvalue: boolean, key: any, record: TableData, event: InputEventCallback when a row is selected.0.1.0
selectAllvalue: boolean, event: InputEventCallback when select-all is triggered.0.1.0
selectedChangevalue: any[]0.1.0
update:expandedKeysvalue: any[]Callback when expandedKeys is updated.0.1.0
expandvalue: boolean, key: any, record: TableData, event: MouseEventCallback when a row is expanded.0.1.0
expandedChangevalue: any[]Callback when the expanded rows change.0.1.0
update:filterValuevalue: FilterValueCallback when filterValue is updated.0.1.0
filterSelectvalue: any[], key: string | number | symbol, option: TableFilterOption | string, column: TableColumn, event: InputEventCallback when an option in the filter is selected.0.1.0
filterConfirmkey: string | number | symbol, event: MouseEventCallback when filter selection is confirmed.0.1.0
filterResetkey: string | number | symbol, event: MouseEventCallback when filter selection is reset.0.1.0
filterChangevalue: FilterValueCallback when the filter selection changes.0.1.0
update:sortOrdervalue: SortOrderCallback when sortOrder is updated.0.1.0
sortSelectvalue: 'asc' | 'desc' | 'none', key: string | number | symbol, column: TableColumn, event: MouseEventCallback when a sort direction is selected.0.1.0
sortOrderChangevalue: SortOrderCallback when the sort order changes.0.1.0
cellMouseentercolumn: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEventCallback when the mouse enters a cell.0.1.0
cellMouseleavecolumn: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEventCallback when the mouse leaves a cell.0.1.0
cellClickcolumn: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEventCallback when a cell in the data area is clicked.0.1.0
cellDblclickcolumn: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEventCallback when a cell in the data area is double-clicked.0.1.0
cellContextmenucolumn: TableColumn, record: TableData, colIndex: number, rowIndex: number, event: MouseEventCallback when a cell in the data area is right-clicked (context menu).0.1.0
headCellClickcolumn: TableColumn, indexPath: number[], event: MouseEventCallback when a header cell is clicked.0.1.0
headCellDblclickcolumn: TableColumn, indexPath: number[], event: MouseEventCallback when a header cell is double-clicked.0.1.0
headCellContextmenucolumn: TableColumn, indexPath: number[], event: MouseEventCallback when a header cell is right-clicked (context menu).0.1.0
rowClickrecord: TableData, rowIndex: number, event: MouseEventCallback when a row in the data area is clicked.0.1.0
rowDblclickrecord: TableData, rowIndex: number, event: MouseEventCallback when a row in the data area is double-clicked.0.1.0
rowContextmenurecord: TableData, rowIndex: number, event: MouseEventCallback when a row in the data area is right-clicked (context menu).0.1.0
update:pagevalue: numberCallback for updating page.0.1.0
update:pageSizevalue: numberCallback for updating pageSize.0.1.0

TableSlots

SlotParameterDescriptionVersion
[columns[].labelSlotName]rowIndex: number, colIndex: number, column: TableColumnSlot for custom content in header cells.0.1.0
[columns[].slotName]colIndex: number, rowIndex: number, column: TableColumn, record: TableDataSlot for custom content in table cells.0.1.0
expandrowIndex: number, record: TableDataSlot for expanded row content.0.1.0

TableExpose

AttributeTypeOptionalDefaultDescriptionVersion
getCurrentData() => TableData[]FalseGet the currently displayed data of the table after sorting and filtering.0.1.0
getPaginatedData() => TableData[]FalseGet the currently displayed data of the table after pagination.0.1.0
select(key: any | any[], value: boolean) => Promise<void>FalseHandle the selection state of table rows.0.1.0
selectAll(value: boolean, crossPage?: boolean, ignoreDisabled?: boolean) => Promise<void>FalseSelect all/deselect all table rows. crossPage indicates cross-page selection, default is false. ignoreDisabled controls whether to ignore rows with a disabled state, default is true.0.1.0
clearSelect() => Promise<void>FalseClear the selection state of all rows.0.1.0
expand(key: any | any[], value: boolean) => Promise<void>FalseHandle the expand/collapse state of table rows.0.1.0
clearExpand() => Promise<void>FalseClear the expanded/collapsed state of all rows.0.1.0
filter(key: number | string | symbol, value: any[]) => Promise<void>FalseHandle the filter state of table columns.0.1.0
clearFilter() => Promise<void>FalseClear the filter state of all columns.0.1.0
sort(key: number | string | symbol, value: 'none' | 'asc' | 'desc') => Promise<void>FalseHandle the sort state of table columns.0.1.0
clearSort() => Promise<void>FalseClear the sort state of all columns.0.1.0

TableData

AttributeTypeOptionalDefaultDescriptionVersion
expandboolean | string | (( arg: Pick<TableOptionsArg, 'record' | 'rowIndex'>) => VNode | string | JSX.Element | null | void)TrueConfiguration for expanded row content.0.1.0
disabledbooleanTruefalseWhether row selection is disabled.0.1.0
[x: string | number | symbol]anyTrueOther properties.0.1.0

TableColumn

AttributeTypeOptionalDefaultDescriptionVersion
keynumber | string | symbolFalseUnique identifier for the column.0.1.0
labelstringTrueText displayed in the column header.0.1.0
fieldstringTrueThe data field name corresponding to the cell.0.1.0
widthnumberTrue80Column width setting.0.1.0
minWidthnumberTrue0Minimum width setting for the column.0.1.0
align'left' | 'center' | 'right'True'left'Cell text alignment.0.1.0
fixed'left' | 'right' | 'none'True'none'Column fixed position setting.0.1.0
slotNamestringTrueSlot name for cell content.0.1.0
renderstring | ((arg: TableOptionsArg) => VNode | string | JSX.Element | null | void)TrueCell content render function or name.0.1.0
labelSlotNamestringTrueSlot name for header cell content.0.1.0
labelRenderstring | ((arg: Omit<TableOptionsArg, 'record'>) => VNode | string | JSX.Element | null | void)TrueHeader cell content render function or name.0.1.0
childrenTableColumn[]TrueCollection of child columns, used for multi-level headers.0.1.0
filterableTableFilterableTrueFilter configuration for the column.0.1.0
sortableTableSortableTrueSort configuration for the column.0.1.0
cellPropsRestAttrsTrueCell attributes object.0.1.0
labelCellPropsRestAttrsTrueHeader cell attributes object.0.1.0
contentPropsRestAttrsTrueCell content attributes object.0.1.0
labelContentPropsRestAttrsTrueHeader content attributes object.0.1.0

TableBordered

AttributeTypeOptionalDefaultDescriptionVersion
tablebooleanTruetrueTable outer border.0.1.0
rowbooleanTruetrueTable row horizontal border.0.1.0
colbooleanTruetrueTable column vertical border.0.1.0
headbooleanTruetrueBorder between the header area and the content area.0.1.0
sidebooleanTruetrueBorders on the left and right sides of the table; only takes effect when bordered.table or bordered is true.0.1.0

TableSelection

AttributeTypeOptionalDefaultDescriptionVersion
multiplebooleanTruefalseWhether multiple selection is enabled.0.1.0
showSelectAllbooleanTruetrueWhether to show the select-all button; only effective when selection.multiple is true.0.1.0
selectAllMethod(value: boolean, preState: { value: boolean, indeterminate: boolean }, arg: { originData: TableData[], currentData: TableData[], paginatedData: TableData[], page: number, pageSize: number }) => any[] | Promise<any[]>TrueCustomizes the selectedKeys value updated during a select‑all operation.0.1.0
supersetSelectAllRef'current' | 'all'True'current'Configures the superset referenced for the select‑all checkbox's checked status.0.1.0
labelstringTrueHeader text for the row selection column.0.1.0
widthnumberTrue48Width of the row selection column.0.1.0
minWidthnumberTrue0Minimum width of the row selection column.0.1.0
fixedbooleanTruefalseWhether the row selection column is fixed; forced to the left when there are left-fixed columns.0.1.0
onlyCurrentbooleanTruefalseWhether selected rows should only include rows in the current data prop.0.1.0
cellPropsRestAttrsTrueColumn cell properties object.0.1.0
labelCellPropsRestAttrsTrueColumn header cell properties object.0.1.0
contentPropsRestAttrsTrueColumn cell content properties object.0.1.0
labelContentPropsRestAttrsTrueColumn header content properties object.0.1.0

TableExpandable

AttributeTypeOptionalDefaultDescriptionVersion
defaultExpandAllRowsbooleanTruefalseWhether all rows are expanded by default.0.1.0
labelstringTrueHeader text for the expand-button column.0.1.0
widthnumberTrue48Width of the expand-button column.0.1.0
minWidthnumberTrue0Minimum width of the expand-button column.0.1.0
fixedbooleanTruefalseWhether the expand-button column is fixed; forced to the left when there are left-fixed columns.0.1.0
cellPropsRestAttrsTrueColumn cell properties object.0.1.0
labelCellPropsRestAttrsTrueColumn header cell properties object.0.1.0
contentPropsRestAttrsTrueColumn cell content properties object.0.1.0
labelContentPropsRestAttrsTrueColumn header content properties object.0.1.0

TableSummary

AttributeTypeOptionalDefaultDescriptionVersion
dataTableData | TableData[]TrueSummary row data.0.1.0
placement'end' | 'start'True'end'Placement of the summary row.0.1.0
summaryTextstring | string[]TrueText for the first column of the summary row; if an array, applies to the corresponding indexed summary row; if a string, applies to all summary rows.0.1.0
fixedbooleanTruetrueWhether the summary row is fixed; when the summary row is placed at the start of the data area, fixing the summary row requires fixedHead to be true.0.1.0
spanMethod(options: TableOptionsArg) => void | { colspan?: number, rowspan?: number }TrueMethod for merging cells in the summary row.0.1.0

TableFilterable

AttributeTypeOptionalDefaultDescriptionVersion
filterOptions(string | TableFilterOption)[]TrueFilter options for the column.0.1.0
filterMethod(filteredValue: any[], record: TableData, field?: string) => booleanTrueFunction to determine whether a record matches the selected filters.0.1.0
defaultFilterValueany[] | nullTrueDefault filter values.0.1.0
multiplebooleanTruefalseWhether multiple filter options can be selected.0.1.0
popoverPropsOmit<PopoverProps, 'visible' | 'content'> & EmitEvent<PopoverEvents>TrueAdditional popover properties for the filter.0.1.0

TableSortable

AttributeTypeOptionalDefaultDescriptionVersion
orders('asc' | 'desc')[] | Readonly<('asc' | 'desc')[]>TrueSort directions available for the column.0.1.0
sortMethod'custom' | ((a: TableData, b: TableData, order: 'asc' | 'desc', field?: string) => number)TrueSort method for the column.0.1.0
defaultSortOrder'asc' | 'desc' | 'none' | nullTrueDefault sort direction for the column.0.1.0
multiplebooleanTruefalseWhether multi-column sorting is enabled.0.1.0
prioritynumberTrue0Priority for multi-column sorting; higher values take precedence.0.1.0

TablePagination

The paginateMethod field controls the pagination method inside the table component. When set to 'auto', the table will paginate the data based on the page capacity. When set to 'custom', no such behavior will occur; this configuration is often used for backend pagination.

ts
export type TablePagination = {
	paginateMethod?: 'custom' | 'auto'
} & PaginationProps &
	EmitEvent<PaginationEvents> &
	RestAttrs

TableOptionsArg

ts
export type TableOptionsArg = {
	rowIndex: number
	colIndex: number
	record: TableData
	column: TableColumn
}

Option

ts
export interface Option<T = any> {
	value: T
	label: string
}

TableFilterOption

ts
export interface TableFilterOption<T = any> extends Option<T> {
	disabled?: boolean
	key?: string | number | symbol
}

SortOrder, FilterValue

ts
export type SortOrder = {
	[key: string | number | symbol]: 'asc' | 'desc' | 'none' | null | undefined
}

export type FilterValue = {
	[key: string | number | symbol]: any[] | null | undefined
}

RestAttrs

ts
import type { StyleValue } from 'vue'

export type VueClassValue = string | Record<string, any> | VueClassValue[]
export type VueStyleValue = StyleValue

export type RestAttrs = {
	style?: VueStyleValue | null
	class?: VueClassValue | null
	[x: string]: any
}

EmitEvent

ts
export type EmitEvent<T extends Record<string, any>> = {
	[K in keyof T as `on${Capitalize<K & string>}`]?: (...args: T[K]) => void
}