25 KiB

DataTable Component Documentation

Overview

A feature-rich data table component built with PrimeVue's DataTable. This component provides advanced functionality including server-side pagination, sorting, manual filtering with apply buttons, row selection, page data caching, and customizable column types with persistent state management.

Basic Usage

<template>
  <DataTable
    :columns="tableColumns"
    :data="tableData"
    :tableActions="tableActions"
    table-name="my-table"
    @row-click="handleRowClick"
  />
</template>

<script setup>
import { ref } from "vue";
import DataTable from "./components/common/DataTable.vue";

const tableColumns = ref([
  {
    fieldName: "name",
    label: "Name",
    sortable: true,
    filterable: true,
  },
  {
    fieldName: "status",
    label: "Status",
    type: "status",
    sortable: true,
    filterable: true,
  },
]);

const tableData = ref([
  { id: 1, name: "John Doe", status: "completed" },
  { id: 2, name: "Jane Smith", status: "in progress" },
]);

const tableActions = ref([
  {
    label: "Add Item",
    action: () => console.log("Add clicked"),
    icon: "pi pi-plus",
    style: "primary",
    // Global action - always available
  },
  {
    label: "View Details",
    action: (rowData) => console.log("View:", rowData),
    icon: "pi pi-eye",
    style: "info",
    requiresSelection: true,
    // Single selection action - enabled when exactly one row selected
  },
  {
    label: "Edit",
    action: (rowData) => console.log("Edit:", rowData),
    icon: "pi pi-pencil",
    style: "secondary",
    rowAction: true,
    // Row action - appears in each row's actions column
  },
]);

const handleRowClick = (event) => {
  console.log("Row clicked:", event.data);
};
</script>

Props

columns (Array) - Required

  • Description: Array of column configuration objects that define the table structure
  • Type: Array<Object>
  • Required: true

data (Array) - Required

  • Description: Array of data objects to display in the table
  • Type: Array<Object>
  • Required: true

tableName (String) - Required

  • Description: Unique identifier for the table, used for persistent filter state management
  • Type: String
  • Required: true

totalRecords (Number)

  • Description: Total number of records available on the server (for lazy loading)
  • Type: Number
  • Default: 0

onLazyLoad (Function)

  • Description: Custom pagination event handler for server-side data loading
  • Type: Function
  • Default: null

filters (Object)

  • Description: Initial filter configuration object (used for non-lazy tables)
  • Type: Object
  • Default: { global: { value: null, matchMode: FilterMatchMode.CONTAINS } }

tableActions (Array)

  • Description: Array of action objects that define interactive buttons for the table. Actions can be global (always available), single-selection (enabled when exactly one row is selected), row-specific (displayed per row), or bulk (for multiple selected rows).
  • Type: Array<Object>
  • Default: []

Server-Side Pagination & Lazy Loading

When lazy is set to true, the DataTable operates in server-side mode with the following features:

Automatic Caching

  • Page Data Caching: Previously loaded pages are cached to prevent unnecessary API calls
  • Cache Duration: 5 minutes default expiration time
  • Cache Size: Maximum 50 pages per table with automatic cleanup
  • Smart Cache Keys: Based on page, sorting, and filter combinations

Manual Filter Controls

  • Apply Button: Filters are applied manually via button click to prevent excessive API calls
  • Clear Button: Quick reset of all active filters
  • Enter Key Support: Apply filters by pressing Enter in any filter field
  • Visual Feedback: Shows active filters and pending changes

Quick Page Navigation

  • Page Dropdown: Jump directly to any page number
  • Page Info Display: Shows current record range and totals
  • Persistent State: Page selection survives component re-mounts

Column Configuration

Each column object in the columns array supports the following properties:

Basic Properties

  • fieldName (String, required) - The field name in the data object
  • label (String, required) - Display label for the column header
  • sortable (Boolean, default: false) - Enables sorting for this column
  • filterable (Boolean, default: false) - Enables row-level filtering for this column

Column Types

  • type (String) - Defines special rendering behavior for the column

Available Types:

'status' Type

Renders values as colored tags/badges:

{
  fieldName: 'status',
  label: 'Status',
  type: 'status',
  sortable: true,
  filterable: true
}

Status Colors:

  • 'completed' → Success (green)
  • 'in progress' → Warning (yellow/orange)
  • 'not started' → Danger (red)
  • Other values → Info (blue)
'button' Type

Renders values as clickable buttons:

{
  fieldName: 'action',
  label: 'Action',
  type: 'button'
}

Table Actions Configuration

Table actions allow you to add interactive buttons to your DataTable. Actions can be either global (displayed above the table) or row-specific (displayed in an actions column).

Action Object Properties

Each action object in the tableActions array supports the following properties:

Basic Properties

  • label (String, required) - Display text for the button
  • action (Function, required) - Function to execute when button is clicked
  • icon (String, optional) - PrimeVue icon class (e.g., 'pi pi-plus')
  • style (String, optional) - Button severity: 'primary', 'secondary', 'success', 'info', 'warning', 'danger'
  • size (String, optional) - Button size: 'small', 'normal', 'large'
  • requiresSelection (Boolean, default: false) - When true, action appears above table but is only enabled when exactly one row is selected
  • requiresMultipleSelection (Boolean, default: false) - Determines if action is for bulk operations on selected rows
  • rowAction (Boolean, default: false) - When true, action appears in each row's actions column
  • layout (Object, optional) - Layout configuration for action positioning and styling

Layout Configuration

The layout property allows you to control where and how actions are displayed:

For Top-Level Actions (Global and Single Selection)
layout: {
  position: "left" | "center" | "right", // Where to position in action bar
  variant: "filled" | "outlined" | "text" // Visual style variant
}
For Row Actions
layout: {
  priority: "primary" | "secondary" | "dropdown", // Display priority in row
  variant: "outlined" | "text" | "compact" | "icon-only" // Visual style
}
For Bulk Actions
layout: {
  position: "left" | "center" | "right", // Where to position in bulk action bar
  variant: "filled" | "outlined" | "text" // Visual style variant
}

Action Types

Global Actions (default behavior)

Global actions are displayed above the table and are always available:

{
  label: "Add New Item",
  action: () => {
    // Global action - no row data
    console.log("Opening create modal");
  },
  icon: "pi pi-plus",
  style: "primary"
  // No requiresSelection, requiresMultipleSelection, or rowAction properties
}
Single Selection Actions (requiresSelection: true)

Single selection actions are displayed above the table but are only enabled when exactly one row is selected. They receive the selected row data as a parameter:

{
  label: "View Details",
  action: (rowData) => {
    // Single selection action - receives selected row data
    console.log("Viewing:", rowData.name);
    router.push(`/items/${rowData.id}`);
  },
  icon: "pi pi-eye",
  style: "info",
  requiresSelection: true
}
Row Actions (rowAction: true)

Row actions are displayed in an "Actions" column for each row and receive that row's data as a parameter:

{
  label: "Edit",
  action: (rowData) => {
    // Row action - receives individual row data
    console.log("Editing:", rowData.name);
    openEditModal(rowData);
  },
  icon: "pi pi-pencil",
  style: "secondary",
  rowAction: true
}
Bulk Actions (requiresMultipleSelection: true)

Bulk actions are displayed above the table when rows are selected and receive an array of selected row data:

{
  label: "Delete Selected",
  action: (selectedRows) => {
    // Bulk action - receives array of selected row data
    console.log("Deleting:", selectedRows.length, "items");
    selectedRows.forEach(row => deleteItem(row.id));
  },
  icon: "pi pi-trash",
  style: "danger",
  requiresMultipleSelection: true
}

Example Table Actions Configuration

const tableActions = [
  // Global action - shows above table, always available
  {
    label: "Add Client",
    action: () => modalStore.openModal("createClient"),
    icon: "pi pi-plus",
    style: "primary",
  },
  // Single selection action - shows above table, enabled when exactly one row selected
  {
    label: "View Details",
    action: (rowData) => router.push(`/clients/${rowData.id}`),
    icon: "pi pi-eye",
    style: "info",
    requiresSelection: true,
  },
  // Bulk action - shows when rows selected
  {
    label: "Delete Selected",
    action: (selectedRows) => {
      if (confirm(`Delete ${selectedRows.length} clients?`)) {
        selectedRows.forEach((row) => deleteClient(row.id));
      }
    },
    icon: "pi pi-trash",
    style: "danger",
    requiresMultipleSelection: true,
  },
  // Row actions - show in each row's actions column
  {
    label: "Edit",
    action: (rowData) => editClient(rowData),
    icon: "pi pi-pencil",
    style: "secondary",
    rowAction: true,
  },
  {
    label: "Quick View",
    action: (rowData) => showQuickPreview(rowData),
    icon: "pi pi-search",
    style: "info",
    rowAction: true,
  },
];

Events

rowClick

  • Description: Emitted when a button-type column is clicked
  • Payload: PrimeVue slot properties object containing row data
  • Usage: @row-click="handleRowClick"

lazy-load

  • Description: Emitted when lazy loading is triggered (pagination, sorting, filtering)
  • Payload: Event object with page, sorting, and filter information
  • Usage: @lazy-load="handleLazyLoad"

page-change

  • Description: Emitted when page changes
  • Payload: PrimeVue page event object

sort-change

  • Description: Emitted when sorting changes
  • Payload: PrimeVue sort event object

filter-change

  • Description: Emitted when filters are applied
  • Payload: PrimeVue filter event object
const handleRowClick = (slotProps) => {
  console.log("Clicked row data:", slotProps.data);
  console.log("Row index:", slotProps.index);
};

const handleLazyLoad = async (event) => {
  // event contains: page, rows, sortField, sortOrder, filters
  console.log("Lazy load event:", event);

  // Load data from API based on event parameters
  const result = await Api.getData({
    page: event.page,
    pageSize: event.rows,
    sortField: event.sortField,
    sortOrder: event.sortOrder,
    filters: event.filters,
  });

  // Update component data
  tableData.value = result.data;
  totalRecords.value = result.totalRecords;
};

Features

Pagination

  • Rows per page options: 5, 10, 20, 50
  • Default rows per page: 10
  • Built-in pagination controls

Sorting

  • Multiple column sorting support
  • Removable sort - click to remove sort from a column
  • Sort indicators in column headers

Filtering

  • Manual filter application with Apply/Clear buttons
  • Text-based search for filterable columns
  • Persistent filter state across component re-renders and page navigation
  • Visual filter feedback showing active filters and pending changes
  • Enter key support for quick filter application

Selection

  • Multiple row selection with checkboxes
  • Meta key selection (Ctrl/Cmd + click for individual selection)
  • Unique row identification using dataKey="id"

Scrolling

  • Vertical scrolling with fixed height (70vh)
  • Horizontal scrolling for wide tables
  • Fixed headers during scroll

State Management

  • Persistent filters using Pinia store (useFiltersStore)
  • Automatic filter initialization on component mount
  • Cross-component filter synchronization

Table Actions

  • Global actions displayed above the table for general operations
  • Row-specific actions in dedicated actions column with row data access
  • Bulk actions for selected rows with multi-selection support
  • Customizable button styles with PrimeVue severity levels
  • Icon support using PrimeVue icons
  • Automatic action handling with error catching
  • Disabled state during loading operations
  • Dynamic bulk action visibility based on row selection

Usage Examples

<script setup>
import { ref } from "vue";
import DataTable from "./components/common/DataTable.vue";
import Api from "./api.js";

const columns = [
  { fieldName: "id", label: "ID", sortable: true },
  { fieldName: "name", label: "Name", sortable: true, filterable: true },
  { fieldName: "email", label: "Email", filterable: true },
];

const tableData = ref([]);
const totalRecords = ref(0);
const isLoading = ref(false);

const handleLazyLoad = async (event) => {
  try {
    isLoading.value = true;

    // Convert PrimeVue event to API parameters
    const params = {
      page: event.page,
      pageSize: event.rows,
      sortField: event.sortField,
      sortOrder: event.sortOrder,
    };

    // Convert filters
    const filters = {};
    if (event.filters) {
      Object.keys(event.filters).forEach((key) => {
        if (event.filters[key]?.value) {
          filters[key] = event.filters[key];
        }
      });
    }

    // API call with caching support
    const result = await Api.getPaginatedData(params, filters);

    tableData.value = result.data;
    totalRecords.value = result.totalRecords;
  } catch (error) {
    console.error("Error loading data:", error);
    tableData.value = [];
    totalRecords.value = 0;
  } finally {
    isLoading.value = false;
  }
};
</script>

<template>
  <DataTable
    :data="tableData"
    :columns="columns"
    tableName="myTable"
    :lazy="true"
    :totalRecords="totalRecords"
    :loading="isLoading"
    :onLazyLoad="handleLazyLoad"
    @lazy-load="handleLazyLoad"
  />
</template>

Interactive Table with Actions

<script setup>
import { useRouter } from "vue-router";
import { useModalStore } from "./stores/modal";

const router = useRouter();
const modalStore = useModalStore();

const columns = [
  { fieldName: "name", label: "Name", sortable: true, filterable: true },
  { fieldName: "status", label: "Status", type: "status", sortable: true },
  { fieldName: "email", label: "Email", filterable: true },
];

const data = [
  { id: 1, name: "John Doe", status: "completed", email: "john@example.com" },
  {
    id: 2,
    name: "Jane Smith",
    status: "in progress",
    email: "jane@example.com",
  },
];

const tableActions = [
  // Global action
  {
    label: "Add User",
    action: () => {
      modalStore.openModal("createUser");
    },
    icon: "pi pi-plus",
    style: "primary",
  },
  // Single selection action
  {
    label: "View Details",
    action: (rowData) => {
      router.push(`/users/${rowData.id}`);
    },
    icon: "pi pi-eye",
    style: "info",
    requiresSelection: true,
  },
  // Bulk actions
  {
    label: "Export Selected",
    action: (selectedRows) => {
      exportUsers(selectedRows);
    },
    icon: "pi pi-download",
    style: "success",
    requiresMultipleSelection: true,
  },
  {
    label: "Delete Selected",
    action: (selectedRows) => {
      if (confirm(`Delete ${selectedRows.length} users?`)) {
        bulkDeleteUsers(selectedRows.map((row) => row.id));
      }
    },
    icon: "pi pi-trash",
    style: "danger",
    requiresMultipleSelection: true,
  },
  // Row actions
  {
    label: "Edit",
    action: (rowData) => {
      modalStore.openModal("editUser", rowData);
    },
    icon: "pi pi-pencil",
    style: "secondary",
    rowAction: true,
  },
  {
    label: "Quick Actions",
    action: (rowData) => {
      showQuickActionsMenu(rowData);
    },
    icon: "pi pi-ellipsis-v",
    style: "info",
    rowAction: true,
  },
];

const deleteUser = async (userId) => {
  // API call to delete user
  await Api.deleteUser(userId);
};

const bulkDeleteUsers = async (userIds) => {
  // API call to delete multiple users
  await Api.bulkDeleteUsers(userIds);
};

const exportUsers = (users) => {
  // Export selected users to CSV/Excel
  const csv = generateCSV(users);
  downloadFile(csv, "users.csv");
};

const refreshData = () => {
  // Refresh table data
  location.reload();
};
</script>

<template>
  <DataTable
    :columns="columns"
    :data="data"
    :tableActions="tableActions"
    table-name="users-table"
  />
</template>

Basic Client-Side Table

<script setup>
const columns = [
  { fieldName: "id", label: "ID", sortable: true },
  { fieldName: "name", label: "Name", sortable: true, filterable: true },
  { fieldName: "email", label: "Email", filterable: true },
];

const data = [
  { id: 1, name: "John Doe", email: "john@example.com" },
  { id: 2, name: "Jane Smith", email: "jane@example.com" },
];
</script>

<template>
  <DataTable :data="data" :columns="columns" tableName="basicTable" />
</template>

Status Table

<script setup>
const columns = [
  { fieldName: "task", label: "Task", sortable: true, filterable: true },
  {
    fieldName: "status",
    label: "Status",
    type: "status",
    sortable: true,
    filterable: true,
  },
  { fieldName: "assignee", label: "Assignee", filterable: true },
];

const data = [
  { id: 1, task: "Setup project", status: "completed", assignee: "John" },
  { id: 2, task: "Write tests", status: "in progress", assignee: "Jane" },
  { id: 3, task: "Deploy app", status: "not started", assignee: "Bob" },
];
</script>

<template>
  <DataTable :columns="columns" :data="data" table-name="tasks-table" />
</template>

Interactive Table with Buttons

<script setup>
const columns = [
  { fieldName: "name", label: "Name", sortable: true, filterable: true },
  { fieldName: "status", label: "Status", type: "status", sortable: true },
  { fieldName: "action", label: "Action", type: "button" },
];

const data = [
  { id: 1, name: "Project A", status: "completed", action: "View Details" },
  { id: 2, name: "Project B", status: "in progress", action: "Edit" },
];

const handleRowClick = (slotProps) => {
  const { data, index } = slotProps;
  console.log(`Action clicked for ${data.name} at row ${index}`);
  // Handle the action (navigate, open modal, etc.)
};
</script>

<template>
  <DataTable
    :columns="columns"
    :data="data"
    table-name="projects-table"
    @row-click="handleRowClick"
  />
</template>

Custom Filters

<script setup>
import { FilterMatchMode } from "@primevue/core";

const customFilters = {
  global: { value: null, matchMode: FilterMatchMode.CONTAINS },
  name: { value: "John", matchMode: FilterMatchMode.STARTS_WITH },
};
</script>

<template>
  <DataTable
    :columns="columns"
    :data="data"
    :filters="customFilters"
    table-name="filtered-table"
  />
</template>

Layout-Aware Actions Example

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useModalStore } from '@/stores/modal'

const router = useRouter()
const modalStore = useModalStore()

const columns = [
  { fieldName: 'name', label: 'Client Name', sortable: true, filterable: true },
  { fieldName: 'email', label: 'Email', filterable: true },
  { fieldName: 'status', label: 'Status', type: 'status', sortable: true }
]

const data = ref([
  { id: 1, name: 'Acme Corp', email: 'contact@acme.com', status: 'completed' },
  { id: 2, name: 'Tech Solutions', email: 'info@tech.com', status: 'in progress' }
])

const tableActions = [
  // Left-positioned action with filled style
  {
    label: 'Quick Export',
    icon: 'pi pi-download',
    action: () => exportAllClients(),
    severity: 'success',
    layout: { position: 'left', variant: 'outlined' }
  },
  // Center-positioned bulk action
  {
    label: 'Archive Selected',
    icon: 'pi pi-archive',
    action: (selectedRows) => archiveClients(selectedRows),
    severity: 'warning',
    requiresMultipleSelection: true,
    layout: { position: 'center', variant: 'outlined' }
  },
  // Right-positioned main action
  {
    label: 'Create Client',
    icon: 'pi pi-plus',
    action: () => modalStore.openModal('createClient'),
    severity: 'info',
    layout: { position: 'right', variant: 'filled' }
  },
  // Single selection action
  {
    label: 'Edit Details',
    icon: 'pi pi-pencil',
    action: (rowData) => router.push(`/clients/${rowData.id}/edit`),
    severity: 'secondary',
    requiresSelection: true,
    layout: { position: 'left', variant: 'text' }
  },
  // Primary row action - most important
  {
    label: 'View',
    icon: 'pi pi-eye',
    action: (rowData) => router.push(`/clients/${rowData.id}`),
    severity: 'info',
    rowAction: true,
    layout: { priority: 'primary', variant: 'outlined' }
  },
  // Secondary row action - less important
  {
    label: 'Contact',
    icon: 'pi pi-phone',
    action: (rowData) => initiateContact(rowData),
    severity: 'success',
    rowAction: true,
    layout: { priority: 'secondary', variant: 'text' }
  },
  // Dropdown row action - additional options
  {
    label: 'More',
    icon: 'pi pi-ellipsis-v',
    action: (rowData) => showMoreOptions(rowData),
    rowAction: true,
    layout: { priority: 'dropdown', variant: 'icon-only' }
  }
]

const exportAllClients = () => {
  // Export logic
}

const archiveClients = (clients) => {
  // Archive logic
}

const initiateContact = (client) => {
  // Contact logic
}

const showMoreOptions = (client) => {
  // More options logic
}
</script>

<template>
  <DataTable
    :columns="columns"
    :data="data"
    :tableActions="tableActions"
    table-name="clients-with-layout"
  />
</template>

Store Integration

The component integrates with a Pinia store (useFiltersStore) for persistent filter state:

Store Methods Used

  • initializeTableFilters(tableName, columns) - Initialize filters for a table
  • getTableFilters(tableName) - Get current filters for a table
  • updateTableFilter(tableName, fieldName, value, matchMode) - Update a specific filter

Filter Persistence

  • Filters are automatically saved when changed
  • Filters persist across component re-mounts
  • Each table maintains separate filter state based on tableName

Styling

The component uses PrimeVue's default DataTable styling with:

  • Scrollable layout with fixed 70vh height
  • Responsive design that adapts to container width
  • Consistent spacing and typography
  • Accessible color schemes for status badges

Performance Considerations

Large Datasets

  • Virtual scrolling is not implemented - consider for datasets > 1000 rows
  • Client-side pagination may impact performance with very large datasets
  • Debounced filtering helps with real-time search performance

Memory Management

  • Filter state persistence may accumulate over time
  • Consider implementing filter cleanup for unused tables
  • Component re-rendering is optimized through computed properties

Best Practices

  1. Use unique tableName for each table instance to avoid filter conflicts
  2. Define clear column labels for better user experience
  3. Enable sorting and filtering on searchable/comparable columns
  4. Use appropriate column types (status, button) for better UX
  5. Handle rowClick events for interactive functionality
  6. Consider data structure - ensure id field exists for selection
  7. Test with various data sizes to ensure performance
  8. Use consistent status values for proper badge coloring

Accessibility

The component includes:

  • Keyboard navigation support via PrimeVue
  • Screen reader compatibility with proper ARIA labels
  • High contrast status badges for visibility
  • Focus management for interactive elements
  • Semantic HTML structure for assistive technologies

Browser Support

Compatible with all modern browsers that support:

  • Vue 3 Composition API
  • ES6+ features
  • CSS Grid and Flexbox
  • PrimeVue components

Dependencies

  • Vue 3 with Composition API
  • PrimeVue DataTable, Column, Tag, Button, InputText components
  • @primevue/core for FilterMatchMode
  • Pinia store for state management (useFiltersStore)