Table

useTable connects a page loader to the Table component. Columns are Nuxt UI table columns plus a type for common formats.

Source in the kit: composables/useTable.ts · components/Table/

A table from a loader

examples/table/Basic.vue

Column types: TEXT , NUMBER , DATE , DATE_TIME , IMAGE , COMPONENT .

0 จากทั้งหมด 0

NameEmailSalaryJoined

ไม่พบข้อมูล!

0 จากทั้งหมด 0

<template>
  <Table
    :options="table"
    @pageChange="users.fetchPageChange"
    @search="users.fetchSearch"
  />
</template>

<script lang="ts" setup>
import { type IUser, createUsersLoader } from '~/loaders/users'

const users = createUsersLoader()

// useTable turns a page loader into table options: rows, loading/error/empty states and pagination
const table = useTable<IUser>({
  repo: users,
  options: {
    isEnabledSearch: true,
    searchPlaceholder: 'Search users…',
  },
  columns: () => [
    {
      accessorKey: 'name',
      header: 'Name',
    },
    {
      accessorKey: 'email',
      header: 'Email',
      // TEXT truncates with meta.max and shows "-" for empty values
      type: COLUMN_TYPES.TEXT,
      meta: {
        max: 24,
      },
    },
    {
      accessorKey: 'salary',
      header: 'Salary',
      type: COLUMN_TYPES.NUMBER,
    },
    {
      accessorKey: 'createdAt',
      header: 'Joined',
      // DATE / DATE_TIME follow core.date_format and the Thai-year setting
      type: COLUMN_TYPES.DATE,
    },
  ],
})

// The table only renders: fetching is yours. It asks for pages and searches through its events.
onMounted(() =>
  users.fetchPage(1, '', {
    params: {
      limit: 5,
    },
  })
)
</script>

loaders/users.ts

import { defineStore } from 'pinia'

// One file per API resource: its types, then one store per loader.
// `defineStore` makes the loader a singleton, so a list page and its edit form share the same state.

export interface IUser {
  id: string
  name: string
  email: string
  role: 'admin' | 'editor' | 'viewer'
  salary: number
  isActive: boolean
  avatar: string
  createdAt: string
}

export interface IRole {
  id: string
  name: string
}

// Paginated CRUD: GET /users, GET /users/:id, POST /users, PUT /users/:id, DELETE /users/:id
export const createUsersLoader = () => {
  const options = useRequestOptions()

  return usePageLoader<IUser>({
    baseURL: '/users',
    getBaseRequestOptions: options.getMock,
  })
}

// The store is the shared instance: a list page and the form that edits it see the same state.
// Call createUsersLoader() instead when a component wants a private one (each docs example does).
export const useUsersLoader = defineStore('users', createUsersLoader)

// A plain array: GET /roles
export const useRolesLoader = defineStore('roles', () => {
  const options = useRequestOptions()

  return useListLoader<IRole>({
    url: '/roles',
    getRequestOptions: options.getMock,
  })
})

// A single object, or any one-off request: GET /profile
export const useProfileLoader = defineStore('profile', () => {
  const options = useRequestOptions()

  return useObjectLoader<IUser & { loadedAt: string }>({
    method: 'get',
    url: '/profile',
    getRequestOptions: options.getMock,
  })
})

Custom cells and row actions

examples/table/Cells.vue

Three ways to draw a cell, from lightest to most flexible: type , a cell function, a #key-cell slot. is-pin-action keeps the actions column visible while scrolling.

0 จากทั้งหมด 0

UserRoleSalaryStatus

ไม่พบข้อมูล!

0 จากทั้งหมด 0

<template>
  <Table
    :options="table"
    is-pin-action
    @pageChange="users.fetchPageChange"
  >
    <!-- 1. A slot named `<accessorKey>-cell` takes over one column -->
    <template #name-cell="{ row }">
      <div class="flex items-center gap-2">
        <Avatar
          :src="row.original.avatar"
          size="xs"
        />
        <span class="font-medium">{{ row.original.name }}</span>
      </div>
    </template>

    <template #isActive-cell="{ row }">
      <Badge
        :color="row.original.isActive ? 'success' : 'neutral'"
        variant="subtle"
        :label="row.original.isActive ? 'Active' : 'Inactive'"
      />
    </template>

    <!-- 2. The column called `actions` is the conventional place for row buttons -->
    <template #actions-cell="{ row }">
      <DropdownMenu :items="actionsOf(row.original)">
        <Button
          icon="ph:dots-three-vertical"
          color="neutral"
          variant="ghost"
          size="sm"
        />
      </DropdownMenu>
    </template>
  </Table>
</template>

<script lang="ts" setup>
import { type IUser, createUsersLoader } from '~/loaders/users'

const users = createUsersLoader()
const dialog = useDialog()

const table = useTable<IUser>({
  repo: users,
  columns: () => [
    {
      accessorKey: 'name',
      header: 'User',
    },
    {
      accessorKey: 'role',
      header: 'Role',
      // 3. `cell` is a function for small transformations that do not need a template
      cell: ({ row }) => row.original.role.toUpperCase(),
    },
    {
      accessorKey: 'salary',
      header: 'Salary',
      cell: ({ row }) => NumberHelper.toCurrency(row.original.salary),
    },
    {
      accessorKey: 'isActive',
      header: 'Status',
    },
    {
      accessorKey: 'actions',
      header: '',
    },
  ],
})

const actionsOf = (user: IUser) => [
  {
    label: 'View',
    icon: 'ph:eye',
    onSelect: () =>
      dialog.info({
        title: user.name,
        description: user.email,
      }),
  },
  {
    label: 'Delete',
    icon: 'ph:trash',
    color: 'error' as const,
    // confirm() rejects when the user cancels, so always add a catch
    onSelect: () =>
      dialog
        .confirm({
          title: `Delete ${user.name}?`,
          type: DialogType.ERROR,
        })
        .then(() => users.deleteRun(user.id))
        .catch(() => {}),
  },
]

onMounted(() =>
  users.fetchPage(1, '', {
    params: {
      limit: 5,
    },
  })
)
</script>

TableSimple: data you already have

examples/table/Simple.vue

1 - 2 จากทั้งหมด 3

ItemQtyTotal
Design
12
36,000
Development
40
160,000

1 - 2 จากทั้งหมด 3

<template>
  <TableSimple :options="table" />
</template>

<script lang="ts" setup>
interface IInvoiceLine {
  item: string
  quantity: number
  total: number
}

const lines = ref<IInvoiceLine[]>([
  {
    item: 'Design',
    quantity: 12,
    total: 36000,
  },
  {
    item: 'Development',
    quantity: 40,
    total: 160000,
  },
  {
    item: 'Hosting',
    quantity: 1,
    total: 4500,
  },
])

// No loader: TableSimple renders an array you already have and pages it in the browser when `limit` is set
const table = useTableSimple<IInvoiceLine>({
  items: () => lines.value,
  options: {
    limit: 2,
  },
  columns: () => [
    {
      accessorKey: 'item',
      header: 'Item',
    },
    {
      accessorKey: 'quantity',
      header: 'Qty',
      type: COLUMN_TYPES.NUMBER,
    },
    {
      accessorKey: 'total',
      header: 'Total',
      type: COLUMN_TYPES.NUMBER,
    },
  ],
})
</script>