How to Build a Powerful Generic Table Component with Vue, TypeScript, and Hooks
The article introduces the 100th Table component, explains usage, generic support, defines base properties, shows extensive configuration options, events, hooks, and related components like toolbar and pagination, demonstrating how to create flexible, feature‑rich tables in a Vue/TypeScript project.
1. Usage
We wrapped a slightly different table component as shown in the image below.
No direct column definition passed in
Table and pagination components are separated
The table includes many built‑in event supports
And many more features
Below we briefly describe how we built this table step by step.
2. Generic Support
Because each table usually corresponds to an entity class, the table wrapper supports generics:
<script generic="E extends AirEntity" lang="ts" setup>The AirEntity definition indicates that the data has an ID property, which matches the primary‑key operations needed by the table.
Of course, if you really don’t have an ID, you can use a front‑end UUID instead.
3. Defining Basic Table Props
A backend table should at least contain the following properties:
Table columns
Table data
Loading state
…
Since we use TypeScript + decorators , we support passing columns directly from class decorators marked with @Table():
const props = defineProps({
/** # Table data array */
dataList: { type: Array<E>, required: true },
/** # Display field list */
fieldList: { type: Array<AirTableFieldConfig>, default: () => [] },
/** # Entity class for the table */
entity: { type: Function as unknown as PropType<ClassConstructor<E>>, required: true }
})3.1 dataList
dataListis the required property that holds the table data as a generic array.
3.2 fieldList
fieldListis an optional array of AirTableFieldConfig. If provided, it overrides column definitions extracted from the entity class via @Table().
3.3 entity
entityis the constructor of the entity class and is required to read column definitions from the class. For example, :entity="User" will read @Table() ‑marked properties from User.
After defining these three props, the table can be used like this:
<ATable :data-list="list" :entity="PermissionEntity" />
<script lang="ts" setup>
const list = ref<PermissionEntity[]>([])
</script>You can also pass fieldList manually:
<ATable :data-list="list" :entity="PermissionEntity" />
<script lang="ts" setup>
const list = ref<PermissionEntity[]>([])
const fieldList = computed(() => PermissionEntity.getTableFieldList().filter(item => item.key !== 'isDisabled'))
</script>The PermissionEntity class declares the data structure for each row:
@Model({ label: '权限' })
export class PermissionEntity extends BaseEntity implements ITree {
@Table({ forceShow: true })
@Search()
@Form({ requiredString: true })
@Field({ label: '权限名称' })
name!: string
@Table({ forceShow: true, copyField: true })
@Form({ requiredString: true })
@Field({ label: '权限标识' })
identity!: string
// ... more fields
}This class also contains form and search metadata, making it convenient to manage table, form, and search logic in one place.
4. More Table Props
Beyond the three core props, many business‑specific props are provided:
const props = defineProps({
/** Hide edit button */
hideEdit: { type: Boolean, default: false },
/** Callback to disable edit per row */
disableEdit: { type: Function as PropType<(row: E) => boolean>, default: null },
/** Hide index column */
hideIndex: { type: Boolean, default: false },
/** Hide column selector */
hideFieldSelector: { type: Boolean, default: false },
// ... many more
})Examples include hideEdit, hideDelete, showEnableAndDisable to control button visibility and emit corresponding events.
4.1 hideFieldSelector
When enabled, the table shows a column selector allowing users to choose which columns to display.
4.2 Row Callback Props
Properties like disableEdit and disableDelete accept callbacks to conditionally disable actions per row:
<ATable :disable-delete="row => row.isSystem" :disable-edit="row => row.isSystem" />The row type is the generic entity E of the table.
5. Additional Configurations
Other supported configurations include button permission identifiers, show/hide features, column width settings, extra button configs, lazy loading, tree tables, etc.
6. Table Events
The table emits a set of standard events for CRUD operations and selection:
const emits = defineEmits<{
onDetail: [row: E],
onDelete: [row: E],
onEdit: [row: E],
onSelect: [list: E[]],
onAdd: [row: E],
onSort: [sort?: AirSort],
onDisable: [row: E],
onEnable: [row: E]
}>()These events always return an object containing an ID property.
7. Table Hooks
We provide a base hook airTableHook that contains all table props and events but excludes CRUD logic.
Two derived hooks extend this base:
7.1 useAirTable (standard table)
This hook adds CRUD methods and common utilities. Example usage:
<template>
<ATable v-loading="isLoading" :data-list="response.list" :entity="UserEntity" @on-edit="onEdit" @on-delete="onDelete" />
</template>
<script lang="ts" setup>
const { isLoading, response, onEdit, onDelete } = useAirTable(UserEntity, UserService)
</script>7.2 useAirSelector (selector table)
This hook extends the base hook with selector‑specific functionality:
<template>
<ASelector :editor="UserEditor" :entity="UserEntity" :props="props" :service="UserService" />
</template>
<script lang="ts" setup>
const props = defineProps(airPropsSelector<UserEntity>(new UserEntity()))
</script>To open the selector and get the selected list:
const userList = await AirDialog.selectList(UserSelector)8. Related Components
8.1 Toolbar
The toolbar may contain buttons such as “Add”, “Refresh”, “Batch Operations”, and a search box. It can receive the values returned by useAirTable or useAirSelector to interact with the table.
8.2 Pagination
Pagination is a separate component that can be combined with the table, supporting both paginated and non‑paginated data, as well as tree tables. The response object is wrapped for easy data extraction.
9. Conclusion
We have provided a rich set of table, toolbar, and pagination components together with corresponding hooks, making list‑page development much simpler. Interested readers can explore the full source code in our open‑source project.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
