Skip to content

Table ​

Powerful data table component with integrated search, filtering, sorting, pagination, and row/table-level actions. Supports responsive mobile layout, custom cell formatting, and inline editing with modal forms.

Overview ​

VOvTable provides a comprehensive data display solution with configurable columns, built-in search and filtering capabilities, multi-column sorting, and action handling. Supports dynamic cell formatting with conditional styling, inline forms for editing, and both mobile-friendly card layout and desktop table view. The component handles large datasets with pagination and includes refresh functionality.

Features:

  • Configurable columns with custom formatting and alignment
  • Full-text search across all columns
  • Advanced filtering with form-based interface
  • Multi-column sorting with drag-to-reorder
  • Pagination with next/previous navigation
  • Row-level actions with inline editing via modal forms
  • Table-level actions (add, bulk operations)
  • Cell-level actions with confirmation forms
  • Responsive mobile layout (card view)
  • Custom cell rendering with truncation and detail view
  • Loading state with overlay indicator
  • Conditional cell formatting with validation rules
  • Full TypeScript support

Usage ​

Examples
vue
<template>
  <v-ov-table :options :data :loading @fetch="fetch" @action="action" />
  <v-checkbox v-model="loading" label="Loading" class="ma-1" />
</template>

<script setup lang="ts">
definePage({
  meta: {
    visibility: 'with-role',
    access: 'with-role',
    roles: ['developer'],
  },
})

const loading = ref(false)
const data = ref<OvTableData[]>([])

import jsonData from './sandbox-table.json'

function action(actionName: string, actionData: OvTableData[], value?: OvTableData) {
  console.log('action', actionName, actionData, value)
  if (actionName == 'custom' && value && 'phone' in value)
    value.phone = Math.ceil(Math.random() * 100)
  if (actionName == 'delete' && value && 'status' in value) value.status = 'blocked'
  if (actionName === 'status-all') {
    const newStatus = value?.status
    if (!newStatus) return
    actionData.forEach((item) => {
      item.status = newStatus
    })
  }
}

async function fetch(
  fetchData: OvTableData[],
  offset: number,
  limit: number,
  search: string,
  filter: OvFilterValue,
  sort: string,
) {
  loading.value = true
  await new Promise((resolve) => setTimeout(resolve, 1500))

  let newData = jsonData

  if (search)
    newData = newData.filter((item) =>
      Object.values(item).some((value) =>
        String(value).toLowerCase().includes(search.toLowerCase()),
      ),
    )

  if (Object.keys(filter).length > 0) {
    newData = newData.filter((item) =>
      Object.entries(filter).every(([key, value]) => {
        const itemValue = item[key as keyof typeof item]
        return Array.isArray(value)
          ? value.includes(itemValue as string)
          : String(itemValue).toLowerCase().includes(String(value).toLowerCase())
      }),
    )
  }

  if (sort) {
    const sortArr = sort.split(',')
    newData = newData.sort((a, b) => {
      for (const sortItem of sortArr) {
        const sortKey = (sortItem.startsWith('-') ? sortItem.slice(1) : sortItem) as keyof typeof a
        const sortDir = sortItem.startsWith('-') ? -1 : 1
        if (a[sortKey] < b[sortKey]) return -sortDir
        if (a[sortKey] > b[sortKey]) return sortDir
      }
      return 0
    })
  }

  newData = newData.slice(offset, offset + limit)
  data.value = newData
  console.log('fetch', data.value)
  loading.value = false
}

const options = ref<OvTableOptions>({
  key: 'name',
  columns: [
    { name: 'name' },
    {
      name: 'email',
      format: {
        icon: '$mdiEmail',
        href: 'mailto:{{value}}',
        target: '_blank',
      },
    },
    { name: 'phone', maxLength: 0 },
    { name: 'website', format: { text: 'site' }, maxLength: 20 },
    {
      name: 'status',
      format: [
        {
          rules: { type: 'starts-with', params: 'active' },
          color: 'green',
        },
        { color: 'red' },
      ],
      align: 'center',
    },
    {
      name: 'actions',
      align: 'right',
      actions: [
        {
          name: 'edit',
          format: { icon: '$mdiPencil' },
          form: {
            fields: [
              {
                type: 'text',
                name: 'name',
                label: 'name',
                rules: [{ type: 'required', params: true, message: 'required' }],
              },
              { type: 'text', name: 'email', label: 'email' },
              { type: 'text', name: 'phone', label: 'phone' },
              { type: 'text', name: 'website', label: 'website' },
              { type: 'select', name: 'status', label: 'status', items: ['active', 'blocked'] },
            ],
            actions: [
              { name: 'edit', format: { text: 'submit' } },
              {
                name: 'cancel',
                format: { variant: 'outlined', text: 'cancel', color: 'lime' },
              },
            ],
            actionFormat: {
              color: 'lime',
            },
            actionAlign: 'center',
            actionSubmit: 'edit',
            actionCancel: 'cancel',
            cols: 2,
            autocomplete: 'off',
            focusFirst: true,
          },
        },
        {
          name: 'delete',
          key: 'status',
          format: [
            {
              rules: { type: 'starts-with', params: 'active' },
              icon: '$mdiDelete',
              color: 'red',
            },
            { hidden: true },
          ],
        },
      ],
    },
  ],
  actions: [
    { name: 'add', format: { icon: '$mdiPlus' } },
    {
      name: 'status-all',
      format: { icon: '$mdiListStatus' },
      form: {
        fields: [
          {
            type: 'select',
            name: 'status',
            label: 'status',
            items: ['active', 'blocked'],
            rules: [{ type: 'required', params: true, message: 'required' }],
          },
        ],
        actions: [
          { name: 'status', format: { text: 'submit' } },
          {
            name: 'cancel',
            format: { variant: 'outlined', text: 'cancel', color: 'lime' },
          },
        ],
        actionSubmit: 'status',
        actionCancel: 'cancel',
      },
    },
  ],
  actionFormat: {},
  search: {
    value: 'a',
    label: 'search',
    placeholder: '',
  },
  filter: {
    fields: [
      { type: 'text', name: 'phone', label: 'phone' },
      {
        type: 'select',
        name: 'status',
        label: 'status',
        value: ['active', 'blocked'],
        items: ['active', 'blocked'],
        multiple: true,
      },
    ],
    actions: [{ name: 'custom' }],
    cols: 2,
  },
  sort: [
    { name: 'name', value: 'asc' },
    { name: 'phone', label: 'Phone' },
    { name: 'status', value: 'desc' },
  ],
  maxLength: 40,
})
</script>

API ​

Props ​

PropTypeDefaultDescription
optionsOvTableOptions-Required. Table configuration with columns, actions, and features
dataOvTableData[][]Array of data objects to display in table rows
loadingbooleanfalseShow loading overlay with spinner
t(text?: string) => string(text) => textTranslation/localization function for labels and messages

Events ​

EventPayloadDescription
fetchdata: OvTableData[], offset: number, limit: number, search?: string, filter?: OvFilterValue, sort?: stringEmitted when data needs to be fetched (on mount, page change, search, filter, sort)
actionname: string, data: OvTableData[], value?: OvTableDataEmitted when row action, table action, or cell action is triggered

Exposed Methods ​

MethodParametersDescription
fetchnewPage?: numberManually trigger data fetch for current or specified page

Table Options ​

typescript
interface OvTableOptions {
  // Required
  key: string
  columns: OvTableColumn[]

  // Optional - Display
  columnFormat?: OvFormat | OvFormat[]
  maxLength?: number
  align?: 'left' | 'center' | 'right'
  itemsPerPage?: number
  currentPage?: number

  // Optional - Search
  search?: {
    value?: string
    label?: string
    placeholder?: string
  }

  // Optional - Filtering
  filter?: OvFormOptions

  // Optional - Sorting
  sort?: OvTableSort[]

  // Optional - Actions
  actions?: OvAction[]
  actionFormat?: OvFormat | OvFormat[]

  // Optional - Behavior
  canRefresh?: boolean
}

Column Definition ​

Columns define how data is displayed and edited:

typescript
interface OvTableColumn {
  // Required
  name: string

  // Optional - Display
  title?: string
  align?: 'left' | 'center' | 'right'
  maxLength?: number

  // Optional - Formatting
  format?: OvFormat | OvFormat[]

  // Optional - Actions
  actions?: OvAction[]
  actionFormat?: OvFormat | OvFormat[]
}

Column Properties ​

PropertyTypeDescription
namestringRequired. Unique column identifier matching data object keys
titlestringColumn header text (uses name if not provided)
align'left' | 'center' | 'right'Text alignment (default: left)
maxLengthnumberCharacter limit for display (0 = full content with detail button)
formatOvFormat | OvFormat[]Conditional formatting rules for cell styling
actionsOvAction[]Array of actions available on each cell/row
actionFormatOvFormat | OvFormat[]Default formatting for actions in this column

Column Formatting ​

Cells can be formatted based on their values using validation rules:

typescript
{
  name: 'status',
  title: 'Status',
  align: 'center',
  format: [
    {
      rules: { type: 'starts-with', params: 'active' },
      color: 'green',
      text: 'Active'
    },
    {
      rules: { type: 'starts-with', params: 'inactive' },
      color: 'orange',
      text: 'Inactive'
    },
    { color: 'red', text: 'Blocked' }
  ]
}

Column Actions ​

Actions on columns enable row-level operations:

typescript
{
  name: 'actions',
  title: 'Actions',
  align: 'right',
  actions: [
    {
      name: 'edit',
      format: { icon: '$mdiPencil' },
      form: {
        fields: [
          { type: 'text', name: 'name', label: 'Name' },
          { type: 'email', name: 'email', label: 'Email' }
        ],
        actions: ['edit', 'cancel'],
        actionSubmit: 'edit',
        actionCancel: 'cancel'
      }
    },
    {
      name: 'delete',
      format: { icon: '$mdiDelete', color: 'red' }
    }
  ]
}

Sort Configuration ​

Multi-column sorting with reorderable sort priorities:

typescript
interface OvTableSort {
  name: string
  label?: string
  value?: 'asc' | 'desc'
}

Sort Properties:

PropertyTypeDescription
namestringRequired. Column name to sort by
labelstringSort label (uses name if not provided)
value'asc' | 'desc'Sort direction (undefined = not sorted)

Example:

typescript
sort: [
  { name: 'name', value: 'asc' },     // Sort by name ascending
  { name: 'email' },                  // Available to sort
  { name: 'status', value: 'desc' }   // Sort by status descending
]

Search Configuration ​

Full-text search across all columns:

typescript
search: {
  value: 'initial search term',
  label: 'search.label',              // Translation key or text
  placeholder: 'search.placeholder'
}

When a user enters search text and presses Enter or clicks the search button, the fetch event is emitted with the search query.

Filter Configuration ​

Advanced filtering with form-based interface:

typescript
filter: {
  fields: [
    {
      type: 'text',
      name: 'email',
      label: 'Email'
    },
    {
      type: 'select',
      name: 'status',
      label: 'Status',
      items: ['active', 'blocked'],
      multiple: true,
      value: ['active']
    }
  ],
  actions: [{ name: 'apply' }, { name: 'cancel' }],
  actionSubmit: 'apply',
  actionCancel: 'cancel',
  cols: 2
}

Table-Level Actions ​

Actions that apply to the entire table:

typescript
actions: [
  {
    name: 'add',
    format: { icon: '$mdiPlus', text: 'Add' },
    form: {
      fields: [
        { type: 'text', name: 'name', label: 'Name' }
      ],
      actions: [{ name: 'add' }, { name: 'cancel' }],
      actionSubmit: 'add',
      actionCancel: 'cancel'
    }
  },
  {
    name: 'export',
    format: { icon: '$mdiDownload', text: 'Export' }
  },
  {
    name: 'bulk-status',
    format: { icon: '$mdiListStatus', text: 'Change Status' },
    form: {
      fields: [
        {
          type: 'select',
          name: 'status',
          label: 'New Status',
          items: ['active', 'blocked'],
          required: true
        }
      ],
      actions: [{ name: 'bulk-status' }, { name: 'cancel' }],
      actionSubmit: 'bulk-status',
      actionCancel: 'cancel'
    }
  }
]

Pagination ​

Tables are paginated with configurable items per page:

typescript
{
  itemsPerPage: 10,    // Items to display per page
  currentPage: 1       // Starting page
}

Navigation buttons appear in the table footer:

  • Previous - Disabled on first page
  • Next - Disabled when fewer items than limit returned
  • Refresh - Appears when canRefresh: true

Data Format ​

Table data is an array of objects where object keys match column name properties:

typescript
type OvTableData = Record<string, unknown>

const data: OvTableData[] = [
  {
    name: 'John Doe',
    email: 'john@example.com',
    phone: '+1-555-0123',
    status: 'active'
  },
  {
    name: 'Jane Smith',
    email: 'jane@example.com',
    phone: '+1-555-0456',
    status: 'inactive'
  }
]

Responsive Behavior ​

Desktop Layout:

  • Standard HTML table display
  • All columns visible horizontally (scrollable)
  • Header row with column titles
  • Search, filter, sort controls in secondary header rows

Mobile Layout (automatic):

  • Card-style display (one row per item)
  • Column name as left column, value as right column
  • Stacked layout for easier viewing on small screens
  • All features (search, filter, sort) remain accessible

Cell Value Truncation ​

Long cell values can be truncated with a detail view button:

typescript
{
  name: 'description',
  maxLength: 100    // Show max 100 chars, add detail button
}

When user clicks the "..." detail button, a modal opens showing the full value in a textarea.

Special handling:

  • maxLength: 0 - Always show detail button with full content
  • maxLength not set - Use table-level maxLength (default: 32767, i.e., no truncation)

Format Rules ​

Cells can apply conditional formatting using validation rules (see Form documentation for available rule types):

typescript
format: [
  {
    rules: { type: 'greater-than', params: 100 },
    color: 'success',
    text: 'High'
  },
  {
    rules: { type: 'less-than', params: 10 },
    color: 'error',
    text: 'Low'
  },
  { color: 'warning' }  // Default fallback
]

Action Handling ​

Row Actions: Events with actionName, full data array, and the specific row data.

typescript
@action="(name, data, row) => {
  if (name === 'edit') {
    // Edit row - form data submitted in event
  } else if (name === 'delete') {
    // Delete row
  }
}"

Table Actions: Events with actionName, full data array, and form submission data.

typescript
@action="(name, data, formData) => {
  if (name === 'add') {
    // Add new row - formData contains new row data
  } else if (name === 'bulk-status') {
    // Bulk operation - formData contains operation parameters
  }
}"

Action Key: Use the key property to extract a specific field value for the action:

typescript
{
  name: 'delete',
  key: 'id',              // Pass item.id instead of entire item
  format: { icon: '$mdiDelete' }
}

Styling ​

The table uses Vuetify defaults provider for consistent styling.

Fetch Event Details ​

The fetch event provides all necessary parameters for server-side data handling:

ParameterTypeExample
dataOvTableData[]Current page data
offsetnumber0 for first page, 10 for second (with itemsPerPage: 10)
limitnumber11 (itemsPerPage + 1 to detect more pages)
searchstring"search term" or ""
filterOvFilterValue{ status: ['active', 'blocked'], phone: ['5551234'] }
sortstring"name,status-desc" or ""

Filter Object Format ​

Filters are passed as an object where each key is a field name and the value is always an array of strings:

typescript
type OvFilterValue = Record<string, string[]>

// Examples:
{ status: ['active'] }
{ status: ['active', 'blocked'], phone: ['5551234'] }
{ email: ['user@example.com'] }

Note: Filter values are always arrays, even when filtering by a single value. This provides consistent handling across the API.

Sort String Format ​

Sort order respected with optional - prefix for descending:

name,status            // name asc, then status asc
name,-status           // name asc, then status desc
-name,status,-id       // Combined ascending and descending

Notes ​

  • Column widths are automatically calculated; use CSS classes for custom sizing
  • Search is case-insensitive full-text search across all columns
  • Filter values are case-insensitive partial matches by default
  • Sort preserves the order of columns in the sort array (first sort is primary)
  • Mobile layout automatically activates below Vuetify's md breakpoint
  • Actions on cells show in the cell content area; table actions appear in footer
  • Detail view modal appears for truncated cells and opens readonly textarea
  • Filter state persists until cleared; sort state is shown as chips
  • Refresh button only appears when canRefresh: true and data is loaded
  • All labels, placeholders, and action text respect the t translation function
  • Loading overlay prevents interaction during data fetch operations
  • The fetch method can be called manually via template ref to refresh data

Composables ​

useTableFetch ​

Encapsulates server-side data fetching for tables with search, filtering, and sorting.

typescript
const { loading, data, fetch: fetchSettings } = useTableFetch({
  endpoint: 'adm/settings/',
  responseKey: 'settings'
})

Options:

  • endpoint (string, required): API endpoint for fetching data
  • responseKey (string, required): Key in response containing data array
  • filter (OvFilterValue, optional): Pre-applied filters
  • search (string, optional): Pre-applied search term

Returns: { loading, data, fetch }

useFormAction ​

Handles form-based actions with automatic error handling and loading state.

typescript
const { action: actionSettings } = useFormAction({
  endpoint: 'adm/setting/'
})

Options:

  • endpoint (string, required): API endpoint for the action

Returns: { loading, action }

The action function accepts: (name, item, value?, callback?) where callback receives errors if present.

Multiple Actions:

typescript
const { action: actionSettings } = useFormAction({ endpoint: 'adm/setting/' })
const { action: actionUsers } = useFormAction({ endpoint: 'adm/user/' })