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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
How to Build a Powerful Generic Table Component with Vue, TypeScript, and Hooks

1. Usage

We wrapped a slightly different table component as shown in the image below.

image.png
image.png

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

dataList

is the required property that holds the table data as a generic array.

3.2 fieldList

fieldList

is an optional array of AirTableFieldConfig. If provided, it overrides column definitions extracted from the entity class via @Table().

3.3 entity

entity

is 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)

2222.png
2222.png

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)

33333.png
33333.png

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.

TypeScriptVuehookstable component
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.