Components Reference

Table Components

Data table components with sorting, filtering, and pagination

CroutonTable

A powerful data table component with sorting, filtering, pagination, and row selection. Used internally by CroutonCollection for table layout mode.

Note: CroutonTable is typically used through CroutonCollection's layout="table" prop. Use this component directly only when you need fine-grained control over table behavior.

Props

interface TableProps {
  // Required props
  columns: TableColumn[]                     // Column definitions
  rows: any[]                               // Data rows to display
  collection: string                        // Collection name for CRUD operations

  // Optional props
  serverPagination?: boolean                // Enable server-side pagination (default: false)
  paginationData?: PaginationData | null    // Pagination metadata (default: null)
  refreshFn?: () => Promise<void>           // Refresh function for server pagination
  sortable?: boolean | SortableOptions      // Enable drag-and-drop row reordering (default: false)
  hideDefaultColumns?: {                    // Hide automatically-added columns
    createdAt?: boolean
    updatedAt?: boolean
    createdBy?: boolean
    updatedBy?: boolean
    actions?: boolean
  }
}

// Sortable Options
interface SortableOptions {
  handle?: boolean     // Show drag handle column (default: true)
  animation?: number   // SortableJS animation duration in ms (default: 150)
  disabled?: boolean   // Temporarily disable drag-and-drop (default: false)
}

// Column Definition
interface TableColumn {
  id?: string
  accessorKey?: string                      // Field key in data
  header: string | ((props: any) => any)   // Column header text or component
  cell?: (props: any) => any               // Custom cell renderer
  sortable?: boolean                        // Enable sorting (default: false)
  enableSorting?: boolean                   // Alternative sorting flag
  enableHiding?: boolean                    // Allow hiding column
}

// Pagination Data
interface PaginationData {
  currentPage: number
  pageSize: number
  totalItems: number
  totalPages?: number
  sortBy?: string
  sortDirection?: 'asc' | 'desc'
}

Props Details

PropTypeDefaultDescription
columnsTableColumn[]requiredColumn definitions for table headers and cells
rowsany[]requiredArray of data objects to display
collectionstringrequiredCollection name (used for CRUD modal actions)
serverPaginationbooleanfalseUse server-side pagination instead of client-side
paginationDataPaginationData | nullnullPagination metadata (required for server pagination)
refreshFn() => Promise<void>undefinedFunction to refresh data (required for server pagination)
hideDefaultColumnsobject{}Control visibility of auto-generated columns

Slots

header (Pass-through)

Customize the entire header section:

<template #header>
  <CroutonTableHeader
    :collection="collection"
    :create-button="true"
  />
</template>

Dynamic Cell Slots

All slots are passed through to Nuxt UI's UTable for custom cell rendering:

<template #location-cell="{ row }">
  <CroutonItemCardMini
    :id="row.original.location"
    collection="locations"
  />
</template>

<template #status-cell="{ row }">
  <UBadge
    :color="row.original.status === 'active' ? 'green' : 'gray'"
  >
    {{ row.original.status }}
  </UBadge>
</template>

Pre-defined Column Slots

CroutonTable provides default renderers for common columns (can be hidden via hideDefaultColumns):

  • createdBy-cell - Shows user avatar and name (via CroutonUsersCardMini)
  • createdAt-cell - Formatted date/time
  • updatedBy-cell - Shows user avatar and name
  • updatedAt-cell - Formatted date/time
  • actions-cell - Edit and delete buttons (via CroutonItemButtonsMini)

Basic Usage

Query Examples: For complete useCollectionQuery patterns, see Querying Data.
<template>
  <CroutonTable
    :collection="collection"
    :columns="columns"
    :rows="users"
  >
    <template #header>
      <CroutonTableHeader
        :collection="collection"
        :create-button="true"
      />
    </template>
  </CroutonTable>
</template>

<script setup lang="ts">
const { items: users } = await useCollectionQuery('users')

const columns = [
  { accessorKey: 'name', header: 'Name', sortable: true },
  { accessorKey: 'email', header: 'Email', sortable: true },
  { accessorKey: 'role', header: 'Role' }
]
</script>

Features

Sorting

Client-side and server-side sorting on sortable columns:

<script setup lang="ts">
const columns = [
  { accessorKey: 'name', header: 'Name', sortable: true },
  { accessorKey: 'createdAt', header: 'Created', sortable: true },
  { accessorKey: 'email', header: 'Email' }  // Not sortable
]
</script>

<template>
  <CroutonTable
    collection="users"
    :columns="columns"
    :rows="users"
  />
</template>

Behavior:

  • Click column header to sort
  • Click again to reverse direction
  • Default sort: createdAt descending
  • Server pagination: triggers API call with sortBy and sortDirection

Search and Filtering

Built-in search bar with debounced input (300ms):

<template>
  <CroutonTable
    collection="bookings"
    :columns="columns"
    :rows="bookings"
  >
    <template #header>
      <CroutonTableHeader
        :collection="collection"
        :create-button="true"
      />
      <!-- Search component automatically included -->
    </template>
  </CroutonTable>
</template>

Search behavior:

  • Case-insensitive string matching
  • Searches across all visible columns
  • Filters rows in real-time
  • Resets to page 1 on new search

Pagination

Client-side pagination (default):

<template>
  <CroutonTable
    collection="products"
    :columns="columns"
    :rows="allProducts"
  />
</template>

Server-side pagination:

<template>
  <CroutonTable
    collection="users"
    :columns="columns"
    :rows="users"
    server-pagination
    :pagination-data="paginationData"
    :refresh-fn="refresh"
  />
</template>

<script setup lang="ts">
const page = ref(1)
const pageSize = ref(10)

const { data: response, refresh } = await useFetch('/api/users', {
  query: { page, pageSize }
})

const users = computed(() => response.value?.items || [])
const paginationData = computed(() => ({
  currentPage: response.value?.page || 1,
  pageSize: response.value?.pageSize || 10,
  totalItems: response.value?.total || 0,
  totalPages: Math.ceil((response.value?.total || 0) / pageSize.value)
}))
</script>

Page sizes: 5, 10, 20, 30, 40

Row Selection

Select rows with checkboxes for bulk operations:

<template>
  <CroutonTable
    collection="users"
    :columns="columns"
    :rows="users"
  >
    <template #header>
      <div class="flex items-center justify-between p-4">
        <CroutonTableHeader :collection="collection" />
        <UButton
          v-if="selectedRows.length > 0"
          color="red"
          @click="handleBulkDelete"
        >
          Delete {{ selectedRows.length }} items
        </UButton>
      </div>
    </template>
  </CroutonTable>
</template>

<script setup lang="ts">
const selectedRows = ref([])

const handleBulkDelete = async () => {
  const ids = selectedRows.value.map(row => row.id)
  // Perform bulk delete
}
</script>

Features:

  • Select individual rows
  • Select all rows (header checkbox)
  • Selected count badge
  • Bulk delete button in TableActions

Column Visibility

Toggle column visibility via dropdown menu:

<template>
  <CroutonTable
    collection="bookings"
    :columns="columns"
    :rows="bookings"
  >
    <template #header>
      <CroutonTableHeader :collection="collection" />
      <!-- Column visibility dropdown in TableActions -->
    </template>
  </CroutonTable>
</template>

Default hidden: id column

Toggle location: TableActions component (eye icon dropdown)

Hide Default Columns

Control which auto-generated columns appear:

<template>
  <CroutonTable
    collection="products"
    :columns="columns"
    :rows="products"
    :hide-default-columns="{
      createdAt: true,
      updatedAt: true,
      createdBy: true,
      updatedBy: true,
      actions: false  // Keep actions column
    }"
  />
</template>

Custom Cell Rendering

Use slots for rich cell content:

<template>
  <CroutonTable
    collection="bookings"
    :columns="columns"
    :rows="bookings"
  >
    <!-- Related entity -->
    <template #location-cell="{ row }">
      <CroutonItemCardMini
        :id="row.original.location"
        collection="locations"
      />
    </template>

    <!-- Date formatting -->
    <template #date-cell="{ row }">
      <CroutonDate :date="row.original.date" format="long" />
    </template>

    <!-- Status badge -->
    <template #status-cell="{ row }">
      <UBadge
        :color="row.original.status === 'confirmed' ? 'green' : 'yellow'"
      >
        {{ row.original.status }}
      </UBadge>
    </template>

    <!-- Avatar group -->
    <template #attendees-cell="{ row }">
      <UAvatarGroup :max="3">
        <UAvatar
          v-for="attendee in row.original.attendees"
          :key="attendee.id"
          :alt="attendee.name"
          :src="attendee.avatar"
          size="xs"
        />
      </UAvatarGroup>
    </template>
  </CroutonTable>
</template>

Loading States

Automatic loading overlay during server pagination:

<template>
  <CroutonTable
    collection="users"
    :columns="columns"
    :rows="users"
    server-pagination
    :pagination-data="paginationData"
    :refresh-fn="refresh"
  />
  <!-- Loading overlay appears automatically during refresh -->
</template>

Loading behavior:

  • Semi-transparent overlay
  • Spinner icon
  • Table fades to 50% opacity
  • Triggered on page change, sort, or refresh

Usage Examples

Basic Table with Pagination

<template>
  <CroutonTable
    collection="bookings"
    :columns="columns"
    :rows="bookings"
    server-pagination
    :pagination-data="paginationData"
    :refresh-fn="refresh"
  />
</template>

<script setup lang="ts">
const { items: bookings, refresh } = await useCollectionQuery('bookings')

const paginationData = computed(() => ({
  currentPage: 1,
  pageSize: 20,
  totalItems: bookings.value.length,
  totalPages: Math.ceil(bookings.value.length / 20)
}))

const columns = [
  { accessorKey: 'name', header: 'Name', sortable: true },
  { accessorKey: 'date', header: 'Date', sortable: true },
  { accessorKey: 'status', header: 'Status' }
]
</script>

Custom Header with Actions

<template>
  <CroutonTable collection="bookings">
    <template #header>
      <div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-900">
        <h2 class="text-lg font-semibold">Bookings</h2>
        <div class="flex items-center gap-4">
          <UButton
            @click="open('create', 'bookings')"
            icon="i-heroicons-plus"
            color="primary"
          >
            New Booking
          </UButton>
          <CroutonTableSearch />
        </div>
      </div>
    </template>
  </CroutonTable>
</template>

<script setup lang="ts">
const { open } = useCrouton()
</script>

Custom Cell Renderers

<template>
  <CroutonTable collection="bookings">
    <!-- Reference item with CardMini -->
    <template #location-cell="{ row }">
      <CroutonItemCardMini
        :id="row.original.location"
        collection="locations"
      />
    </template>

    <!-- Formatted date -->
    <template #date-cell="{ row }">
      <CroutonDate :date="row.original.date" format="medium" />
    </template>

    <!-- Status badge with color -->
    <template #status-cell="{ row }">
      <UBadge
        :color="getStatusColor(row.original.status)"
        variant="subtle"
      >
        {{ row.original.status }}
      </UBadge>
    </template>
  </CroutonTable>
</template>

<script setup lang="ts">
const getStatusColor = (status: string) => {
  const colors = {
    confirmed: 'green',
    pending: 'yellow',
    cancelled: 'red'
  }
  return colors[status] || 'gray'
}
</script>

Sub-Components

CroutonTable uses several sub-components that can be customized:

  • CroutonTableHeader - Header with title and create button
  • CroutonTableSearch - Search input with debouncing
  • CroutonTablePagination - Pagination controls
  • CroutonTableActions - Bulk actions and column visibility
  • CroutonItemButtonsMini - Edit/delete buttons for rows

See Table Components for detailed documentation.

Composables Used

CroutonTable leverages these composables internally:

  • useTableData - Data filtering, searching, pagination logic
  • useTableColumns - Column management with default columns
  • useCrouton - Modal/form state management
  • useT - Translation/i18n support

See Composables Reference for details.

Troubleshooting

Sorting Not Working

If column sorting doesn't respond:

  1. Check sortable prop: Must be true on column definition
  2. Server pagination: Ensure refreshFn triggers API call with sort params
  3. Column key: Verify accessorKey matches data field name

Search Not Filtering

If search doesn't filter results:

  1. Check data structure: Search works on string fields only
  2. Server pagination: Implement search on backend, not client-side
  3. Case sensitivity: Search is case-insensitive by default

Pagination Not Updating

If pagination controls don't work:

  1. Server pagination: Must provide paginationData and refreshFn
  2. Total items: Ensure totalItems in paginationData is correct
  3. Page change: Verify refreshFn is called on page change


Table Components

These four components work together to provide a complete table interface experience. They are designed to be used within CroutonTable or custom table layouts.

TableHeader

Dashboard navbar header with optional create button functionality for collection tables. Displays the collection title and allows users to trigger the creation of new items.

Props

interface TableHeaderProps {
  title?: string          // Display title for the header (default: '')
  collection?: string     // Collection name for routing/actions (default: '')
  createButton?: boolean  // Show/hide create button (default: false)
}
PropTypeDefaultDescription
titlestring''Title text displayed in the left section of the navbar
collectionstring''Collection identifier used for formatting and routing (e.g., 'users', 'articles')
createButtonbooleanfalseControls visibility of the "Create" button in the right section

Slots

#extraButtons

Located in the right section, before the create button. Allows injection of additional action buttons.

<TableHeader collection="users" :create-button="true">
  <template #extraButtons>
    <UButton icon="i-lucide-filter">Filter</UButton>
    <UButton icon="i-lucide-download">Export</UButton>
  </template>
</TableHeader>

Features

Automatic Collection Name Formatting Uses useFormatCollections().collectionWithCapitalSingular() to convert collection names (e.g., "articles" → "Article")

Responsive Create Button Label Shows full label on medium+ screens, abbreviated on mobile:

  • Mobile: "Create"
  • Desktop: "Create Collection Name" (e.g., "Create Article")

Integrated Modal Triggering Calls useCrouton().open('create', collection) to open create modal

Basic Usage

<template>
  <UDashboardPanel>
    <template #header>
      <TableHeader
        :collection="collection"
        :create-button="true"
        title="User Management"
      />
    </template>
  </UDashboardPanel>
</template>

<script setup lang="ts">
const collection = 'users'
</script>

With Extra Buttons

<template>
  <TableHeader
    collection="articles"
    :create-button="true"
  >
    <template #extraButtons>
      <UButton
        icon="i-lucide-filter"
        color="gray"
        variant="ghost"
        @click="openFilters"
      >
        Filter
      </UButton>
      <UButton
        icon="i-lucide-download"
        color="gray"
        variant="ghost"
        @click="exportData"
      >
        Export
      </UButton>
    </template>
  </TableHeader>
</template>

<script setup lang="ts">
const openFilters = () => {
  // Open filter modal
}

const exportData = () => {
  // Export table data
}
</script>

Integration with CroutonTable

TableHeader is automatically used when the create prop is set on CroutonTable:

<template>
  <CroutonTable
    :collection="collection"
    :rows="rows"
    :columns="columns"
    :create="true"  <!-- TableHeader with create button -->
  />
</template>

TableSearch

Debounced search input component for filtering table data. Implements best practices for search UX by preventing excessive API calls during typing.

Props

interface TableSearchProps {
  modelValue: string      // Current search value (required)
  placeholder?: string    // Input placeholder text (default: 'Search...')
  debounceMs?: number    // Debounce delay in milliseconds (default: 300)
}
PropTypeDefaultDescription
modelValuestringrequiredTwo-way bound search value (v-model)
placeholderstring'Search...'Placeholder text shown in empty input
debounceMsnumber300Milliseconds to wait before emitting search value

Events

emit('update:modelValue', value: string)  // Emitted after debounce period
EventPayloadDescription
update:modelValuestringEmitted when user stops typing (after debounce delay). Enables v-model usage.

Features

Automatic Debouncing Uses VueUse's useDebounceFn to prevent excessive updates during typing

v-model Compatible Implements Vue 3's v-model pattern with modelValue prop and update:modelValue emit

Icon Integration Includes a search icon (i-lucide-search) for better UX

Configurable Debounce Allows customization of debounce timing per use case (default 300ms is optimal for most searches)

Responsive Width Uses max-w-sm class for consistent sizing

Basic Usage

<template>
  <div>
    <TableSearch
      v-model="search"
      placeholder="Search users..."
    />
    <CroutonTable
      :rows="filteredRows"
      :columns="columns"
    />
  </div>
</template>

<script setup lang="ts">
const search = ref('')
const { items } = await useCollectionQuery('users')

const filteredRows = computed(() =>
  search.value
    ? items.value.filter(u => u.name.includes(search.value))
    : items.value
)
</script>

Custom Debounce Timing

<template>
  <TableSearch
    v-model="search"
    placeholder="Search products..."
    :debounce-ms="500"
  />
</template>

<script setup lang="ts">
const search = ref('')

// With longer debounce for expensive searches
watch(search, async (value) => {
  await $fetch('/api/expensive-search', {
    query: { q: value }
  })
})
</script>

Integration with CroutonTable

TableSearch is automatically included in CroutonTable when search functionality is enabled:

<template>
  <CroutonTable
    :collection="collection"
    :rows="rows"
    :columns="columns"
    searchable  <!-- Includes TableSearch -->
  />
</template>

Troubleshooting

Search not triggering

  • Problem: Search updates aren't being detected
  • Solution: Ensure you're using v-model with a reactive ref, not a plain variable

Too many API calls

  • Problem: Search is making too many requests
  • Solution: Increase debounceMs to 500-1000ms for expensive operations

Search resets on page change

  • Problem: Search value is lost when navigating
  • Solution: Store search in URL query params or global state

TablePagination

Comprehensive pagination controls including page size selector, current page indicator, and page navigation. Displays contextual information about the current data view (e.g., "Showing 1 to 10 of 100 results").

Props

interface TablePaginationProps {
  page: number            // Current page number (1-indexed) (required)
  pageCount: number       // Items per page (required)
  totalItems: number      // Total number of items across all pages (required)
  loading?: boolean       // Disables controls during loading (default: false)
  pageSizes?: number[]   // Available page size options (default: [5, 10, 20, 30, 40])
}
PropTypeDefaultDescription
pagenumberrequiredCurrent active page (starts at 1)
pageCountnumberrequiredNumber of items shown per page
totalItemsnumberrequiredTotal count of items in the dataset
loadingbooleanfalseWhen true, disables pagination controls
pageSizesnumber[][5, 10, 20, 30, 40]Array of available page size options for dropdown

Events

emit('update:page', value: number)       // Emitted when user changes page
emit('update:pageCount', value: number)  // Emitted when user changes page size
EventPayloadDescription
update:pagenumberEmitted when user navigates to different page (clicking page buttons)
update:pageCountnumberEmitted when user selects different page size from dropdown

Features

Internationalization Support Uses useT() composable for all displayed text, making it fully translatable

Smart Range Display Automatically calculates and displays "Showing X to Y of Z results" with proper edge case handling:

  • Returns 0 when no items exist
  • Correctly handles last page with fewer items than page size

Page Size Selector Dropdown for changing items per page (rows per page)

Accessible Controls UPagination component with proper ARIA labels

Loading State Management Disables controls during data fetching to prevent race conditions

Computed Page Ranges Automatically calculates pageFrom and pageTo based on current page and count

Basic Usage

<template>
  <div>
    <CroutonTable
      :rows="data?.items || []"
      :columns="columns"
    />
    <TablePagination
      :page="page"
      :page-count="pageCount"
      :total-items="data?.pagination?.totalItems || 0"
      :loading="pending"
      @update:page="handlePageChange"
      @update:page-count="handlePageCountChange"
    />
  </div>
</template>

<script setup lang="ts">
const page = ref(1)
const pageCount = ref(10)

const { data, pending, refresh } = await useCollectionQuery({
  collection: 'users',
  pagination: {
    currentPage: page.value,
    pageSize: pageCount.value
  }
})

async function handlePageChange(newPage: number) {
  page.value = newPage
  await refresh()
}

async function handlePageCountChange(newCount: number) {
  pageCount.value = newCount
  page.value = 1 // Reset to first page
  await refresh()
}
</script>

Custom Page Sizes

<template>
  <TablePagination
    :page="page"
    :page-count="pageCount"
    :total-items="total"
    :page-sizes="[10, 25, 50, 100]"
    @update:page="page = $event"
    @update:page-count="handlePageCountChange"
  />
</template>

<script setup lang="ts">
const page = ref(1)
const pageCount = ref(10)
const total = ref(100)

function handlePageCountChange(newCount: number) {
  pageCount.value = newCount
  page.value = 1 // Always reset to first page
  // Fetch new data
}
</script>

With Loading State

<template>
  <div>
    <CroutonTable
      :rows="rows"
      :columns="columns"
      :loading="loading"
    />
    <TablePagination
      :page="page"
      :page-count="pageCount"
      :total-items="totalItems"
      :loading="loading"
      @update:page="loadPage"
      @update:page-count="loadPageCount"
    />
  </div>
</template>

<script setup lang="ts">
const page = ref(1)
const pageCount = ref(10)
const totalItems = ref(0)
const loading = ref(false)
const rows = ref([])

async function loadPage(newPage: number) {
  loading.value = true
  page.value = newPage
  try {
    const { data } = await $fetch('/api/users', {
      query: { page: newPage, limit: pageCount.value }
    })
    rows.value = data.items
    totalItems.value = data.total
  } finally {
    loading.value = false
  }
}

async function loadPageCount(newCount: number) {
  pageCount.value = newCount
  await loadPage(1) // Reset to first page
}
</script>

Integration with CroutonTable

TablePagination is automatically used in CroutonTable when pagination is enabled:

<template>
  <CroutonTable
    :collection="collection"
    :rows="rows"
    :columns="columns"
    :server-pagination="true"
    :pagination-data="paginationData"
  />
</template>

Troubleshooting

Pagination controls disabled

  • Problem: Buttons are grayed out and unclickable
  • Solution: Check if loading prop is set to true

Wrong page range displayed

  • Problem: "Showing 1 to 10 of 0 results" even though items exist
  • Solution: Ensure totalItems prop reflects the actual total count, not just current page items

Page reset doesn't work

  • Problem: Changing page size doesn't reset to page 1
  • Solution: Manually set page.value = 1 in the @update:page-count handler

Page count out of sync

  • Problem: Can navigate beyond the last page
  • Solution: Recalculate total pages: Math.ceil(totalItems / pageCount)

TableActions

Provides batch action controls for table rows, including delete functionality and column visibility management. Implements a standard "bulk actions toolbar" pattern common in data tables.

Props

interface TableActionsProps {
  selectedRows: any[]                    // Array of selected row objects (required)
  collection: string                     // Collection name for routing (required)
  table?: any                           // Table API instance from UTable (optional)
  onDelete?: (ids: string[]) => void   // Custom delete handler (optional)
  onColumnVisibilityChange?: (column: string, visible: boolean) => void  // Optional
}
PropTypeDefaultDescription
selectedRowsany[]requiredArray of selected row objects (from table selection)
collectionstringrequiredCollection identifier for delete operations
tableanyundefinedTanStack Table API instance for column management
onDeleteFunctionundefinedCustom delete handler. If not provided, uses useCrouton().open()
onColumnVisibilityChangeFunctionundefinedCustom handler for column visibility changes

Events

emit('delete', ids: string[])                                     // Emitted when delete is triggered
emit('update:columnVisibility', column: string, visible: boolean) // Emitted when column visibility changes
EventPayloadDescription
deletestring[]Emitted with array of IDs when delete button is clicked
update:columnVisibilitycolumn: string, visible: booleanEmitted when user toggles column visibility in dropdown

Features

Dynamic Delete Button State

  • Shows count of selected items
  • Changes color from neutral to error when items are selected
  • Disabled when no rows selected
  • Pluralizes "item/items" correctly

Flexible Delete Handling

  • If onDelete prop provided: uses custom handler
  • If not provided: opens Crouton delete modal via useCrouton().open('delete', ...)

Column Visibility Management

  • Dropdown menu showing all hideable columns
  • Checkbox toggles for each column
  • Uses TanStack Table API for column management
  • Only shows columns with getCanHide() returning true

Smart Column Formatting Uses upperFirst() from scule to format column IDs to display names (e.g., 'createdAt' → 'CreatedAt')

Internationalization Uses useT() for button labels

Basic Usage

<template>
  <div>
    <div class="flex items-center justify-between gap-3">
      <TableSearch v-model="search" />
      <TableActions
        :selected-rows="selectedRows"
        collection="users"
        :table="tableRef"
        @delete="handleDelete"
      />
    </div>
    <CroutonTable
      v-model:row-selection="selectedRows"
      ref="tableRef"
      :rows="rows"
      :columns="columns"
      selection
    />
  </div>
</template>

<script setup lang="ts">
const selectedRows = ref([])
const tableRef = ref()

async function handleDelete(ids: string[]) {
  console.log('Deleted:', ids)
  // Refresh data
  await refresh()
}
</script>

Custom Delete Handler

<template>
  <TableActions
    :selected-rows="selectedRows"
    collection="users"
    :on-delete="customDelete"
    @delete="refreshTable"
  />
</template>

<script setup lang="ts">
const selectedRows = ref([])

async function customDelete(ids: string[]) {
  const confirmed = await showConfirmation()
  if (!confirmed) return

  await $fetch('/api/bulk-delete', {
    method: 'DELETE',
    body: { ids }
  })

  // Show success notification
  toast.add({
    title: 'Success',
    description: `Deleted ${ids.length} items`
  })
}

async function refreshTable() {
  // Refresh table data
  selectedRows.value = []
}
</script>

Column Visibility Control

<template>
  <div>
    <TableActions
      :selected-rows="selectedRows"
      collection="products"
      :table="tableRef"
      @update:column-visibility="handleColumnVisibilityChange"
    />
    <UTable
      ref="tableRef"
      :data="rows"
      :columns="columns"
    />
  </div>
</template>

<script setup lang="ts">
const tableRef = ref()
const selectedRows = ref([])

function handleColumnVisibilityChange(column: string, visible: boolean) {
  console.log(`Column ${column} visibility: ${visible}`)
  // Optionally persist to local storage or API
  localStorage.setItem(`column-${column}`, String(visible))
}
</script>

With Multiple Actions

<template>
  <div class="flex items-center gap-2">
    <TableActions
      :selected-rows="selectedRows"
      collection="articles"
      :table="tableRef"
      @delete="handleDelete"
    />

    <!-- Additional custom actions -->
    <UButton
      v-if="selectedRows.length > 0"
      color="blue"
      variant="soft"
      @click="bulkPublish"
    >
      Publish {{ selectedRows.length }} item{{ selectedRows.length > 1 ? 's' : '' }}
    </UButton>

    <UButton
      v-if="selectedRows.length > 0"
      color="gray"
      variant="soft"
      @click="bulkExport"
    >
      Export Selected
    </UButton>
  </div>
</template>

<script setup lang="ts">
const selectedRows = ref([])
const tableRef = ref()

async function handleDelete(ids: string[]) {
  // Handle delete
}

async function bulkPublish() {
  const ids = selectedRows.value.map(row => row.id)
  await $fetch('/api/articles/bulk-publish', {
    method: 'POST',
    body: { ids }
  })
  selectedRows.value = []
}

async function bulkExport() {
  const ids = selectedRows.value.map(row => row.id)
  window.location.href = `/api/export?ids=${ids.join(',')}`
}
</script>

Integration with CroutonTable

TableActions is automatically used in CroutonTable when selection is enabled:

<template>
  <CroutonTable
    :collection="collection"
    :rows="rows"
    :columns="columns"
    selection  <!-- Enables row selection and TableActions -->
  />
</template>

Troubleshooting

Delete button always disabled

  • Problem: Button is grayed out even when rows are selected
  • Solution: Ensure selectedRows is a non-empty array

Column visibility not working

  • Problem: Toggling columns doesn't hide/show them
  • Solution: Ensure table prop is passed with the table ref from UTable

Delete confirmation not showing

  • Problem: Items are deleted immediately without confirmation
  • Solution: The component relies on useCrouton() for confirmation. Provide a custom onDelete handler if you need custom confirmation logic.

Wrong items being deleted

  • Problem: Selected items don't match deleted items
  • Solution: Ensure all rows have a unique id property. The component assumes row.id exists.

Type errors with table prop

  • Problem: TypeScript errors about table prop being any
  • Solution: This is a known limitation. The component currently uses any type. You can safely ignore or cast to proper TanStack Table types.



CroutonTableActions

Action buttons for table operations: bulk delete and column visibility toggle.

Props

interface TableActionsProps {
  selectedRows: any[]
  collection: string
  table?: any // TanStack Table instance
  onDelete?: (ids: string[]) => void
}
PropTypeDefaultDescription
selectedRowsany[]-Array of selected table rows
collectionstring-Collection name for delete operation
tableany-TanStack Table API instance
onDeleteFunction-Custom delete handler (optional)

Events

EventPayloadDescription
deletestring[]Emitted when delete is triggered with array of IDs
update:columnVisibilitycolumn: string, visible: booleanEmitted when column visibility changes

Features

  • Bulk Delete: Delete multiple selected rows at once
  • Dynamic Button State: Delete button color changes when items selected
  • Column Visibility: Dropdown menu to show/hide columns
  • i18n Support: Uses translation keys for labels
  • Disabled State: Delete button disabled when no rows selected
  • Custom Delete Handler: Override default delete behavior

Usage

Basic Usage

<template>
  <CroutonTableActions
    :selected-rows="selectedRows"
    :collection="collection"
    :table="tableInstance"
    @delete="handleDelete"
  />
</template>

<script setup lang="ts">
const selectedRows = ref([])
const collection = 'products'
const tableInstance = ref(null)

const handleDelete = (ids: string[]) => {
  console.log('Deleting items:', ids)
}
</script>

With Custom Delete Handler

<template>
  <CroutonTableActions
    :selected-rows="selected"
    collection="users"
    :on-delete="customDeleteHandler"
    @delete="afterDelete"
  />
</template>

<script setup lang="ts">
const selected = ref([])

const customDeleteHandler = async (ids: string[]) => {
  // Custom delete logic
  await api.users.deleteBatch(ids)
  toast.success(`Deleted ${ids.length} users`)
}

const afterDelete = (ids: string[]) => {
  // Refresh table or update UI
  refreshTable()
}
</script>

Column Visibility Control

<template>
  <CroutonTableActions
    :selected-rows="[]"
    collection="orders"
    :table="table"
    @update:columnVisibility="handleColumnToggle"
  />
</template>

<script setup lang="ts">
import { useVueTable } from '@tanstack/vue-table'

const table = useVueTable({
  // ... table config
})

const handleColumnToggle = (column: string, visible: boolean) => {
  console.log(`Column ${column} visibility: ${visible}`)
}
</script>

Integration with CroutonTable

<template>
  <div>
    <CroutonTableActions
      :selected-rows="selectedRows"
      :collection="collection"
      :table="tableRef"
      @delete="refreshData"
    />
    
    <CroutonTable
      v-model:selected-rows="selectedRows"
      :rows="data"
      :columns="columns"
      @table-ready="(t) => tableRef = t"
    />
  </div>
</template>

<script setup lang="ts">
const selectedRows = ref([])
const tableRef = ref(null)
const collection = 'products'
</script>

Delete Button States

StateColorVariantDisabled
No selectionneutralsubtletrue
Items selectederrorsubtlefalse

Column Visibility Menu

The dropdown menu includes:

  • All columns where column.getCanHide() returns true
  • Checkbox for each column
  • Label auto-formatted with upperFirst() (e.g., "firstName" → "FirstName")
  • Click to toggle visibility
<!-- Column menu structure -->
<UDropdownMenu :items="columnVisibilityItems">
  <UButton
    label="Display"
    color="neutral"
    variant="outline"
    trailing-icon="i-lucide-settings-2"
  />
</UDropdownMenu>
Translation Keys: The component uses tString('table.display') for the button label. Ensure your translation system has this key defined.


CroutonTableCheckbox

Wrapper component for table row selection checkbox with indeterminate support.

Props

PropTypeDefaultDescription
modelValueboolean | 'indeterminate'-Checkbox state (checked/unchecked/indeterminate)

Events

EventPayloadDescription
update:modelValueboolean | 'indeterminate'Emitted when checkbox state changes

Features

  • Indeterminate State: Supports three-state checkbox (checked/unchecked/indeterminate)
  • UCheckbox Wrapper: Thin wrapper around Nuxt UI's UCheckbox
  • Type-Safe: TypeScript support for all three states

Usage

Basic Usage

<template>
  <CroutonTableCheckbox v-model="selected" />
</template>

<script setup lang="ts">
const selected = ref(false)
</script>

Indeterminate State

<template>
  <CroutonTableCheckbox v-model="checkboxState" />
</template>

<script setup lang="ts">
const checkboxState = ref<boolean | 'indeterminate'>('indeterminate')

// State can be:
// - true (checked)
// - false (unchecked)
// - 'indeterminate' (partially checked, shown as dash/minus)
</script>

Header "Select All" Checkbox

<template>
  <table>
    <thead>
      <tr>
        <th>
          <CroutonTableCheckbox
            :model-value="headerCheckboxState"
            @update:model-value="handleSelectAll"
          />
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="row in rows" :key="row.id">
        <td>
          <CroutonTableCheckbox v-model="row.selected" />
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script setup lang="ts">
const rows = ref([
  { id: 1, selected: false },
  { id: 2, selected: true },
  { id: 3, selected: false }
])

const headerCheckboxState = computed<boolean | 'indeterminate'>(() => {
  const selectedCount = rows.value.filter(r => r.selected).length
  if (selectedCount === 0) return false
  if (selectedCount === rows.value.length) return true
  return 'indeterminate'
})

const handleSelectAll = (state: boolean | 'indeterminate') => {
  const newState = state === true
  rows.value.forEach(row => row.selected = newState)
}
</script>

With TanStack Table

<template>
  <table>
    <thead>
      <tr v-for="headerGroup in table.getHeaderGroups()">
        <th v-for="header in headerGroup.headers">
          <CroutonTableCheckbox
            v-if="header.id === 'select'"
            :model-value="table.getIsAllRowsSelected() ? true : 
                         table.getIsSomeRowsSelected() ? 'indeterminate' : false"
            @update:model-value="table.toggleAllRowsSelected()"
          />
        </th>
      </tr>
    </thead>
  </table>
</template>

State Values

ValueVisualMeaning
falseEmpty boxNot selected
trueCheckmarkSelected
'indeterminate'Dash/minusPartially selected
TanStack Integration: This component is designed to work seamlessly with TanStack Table's selection state management.


CroutonTableHeader

Header component for data tables with optional create button.

Props

PropTypeDefaultDescription
titlestring''Header title text
collectionstring''Collection name (used for create action)
createButtonbooleanfalseShow create button

Slots

SlotDescription
extraButtonsAdditional buttons in header right area

Features

  • Dashboard Navbar: Built on UDashboardNavbar
  • Create Button: Opens create modal for collection
  • Responsive Label: Hides collection name on mobile (< md)
  • Extra Buttons Slot: Add custom header actions
  • Auto-Formatting: Formats collection name with capital singular

Usage

Basic Usage

<template>
  <CroutonTableHeader
    title="All Products"
    collection="products"
    create-button
  />
</template>

With Extra Buttons

<template>
  <CroutonTableHeader
    title="User Management"
    collection="users"
    create-button
  >
    <template #extraButtons>
      <UButton
        color="neutral"
        variant="outline"
        icon="i-lucide-download"
        @click="exportUsers"
      >
        Export
      </UButton>
      <UButton
        color="neutral"
        variant="outline"
        icon="i-lucide-filter"
        @click="openFilters"
      >
        Filters
      </UButton>
    </template>
  </CroutonTableHeader>
</template>

<script setup lang="ts">
const exportUsers = () => {
  // Export logic
}

const openFilters = () => {
  // Open filters modal
}
</script>

Without Create Button

<template>
  <CroutonTableHeader
    title="System Logs"
    collection="logs"
  />
  <!-- No create button shown -->
</template>

Complete Table Page Example

<template>
  <div>
    <CroutonTableHeader
      title="Products"
      collection="products"
      create-button
    >
      <template #extraButtons>
        <UButton
          icon="i-lucide-upload"
          variant="outline"
          @click="importProducts"
        >
          Import
        </UButton>
      </template>
    </CroutonTableHeader>

    <CroutonTable
      :rows="products"
      :columns="columns"
    />
  </div>
</template>

<script setup lang="ts">
const { open } = useCrouton()
const products = ref([])

const importProducts = () => {
  // Import logic
}
</script>

Create Button Behavior

When clicked, the create button:

  1. Calls useCrouton().open('create', collection)
  2. Opens the create modal/form for the specified collection
  3. Logs debug info to console (collection name, button state)

Responsive Design

Screen SizeButton Label
Mobile (< md)"Create"
Desktop (md+)"Create {CollectionSingular}"

Example:

  • collection="products" → Mobile: "Create", Desktop: "Create Product"
  • collection="users" → Mobile: "Create", Desktop: "Create User"
Collection Formatting: Uses useFormatCollections().collectionWithCapitalSingular() to format collection names (e.g., "products" → "Product").


CroutonTablePagination

Pagination controls for tables with page size selector and result summary.

Props

interface TablePaginationProps {
  page: number
  pageCount: number
  totalItems: number
  loading?: boolean
  pageSizes?: number[]
}
PropTypeDefaultDescription
pagenumber-Current page number (1-indexed)
pageCountnumber-Items per page
totalItemsnumber-Total number of items across all pages
loadingbooleanfalseDisable pagination during loading
pageSizesnumber[][5, 10, 20, 30, 40]Available page size options

Events

EventPayloadDescription
update:pagenumberEmitted when page changes
update:pageCountnumberEmitted when page size changes

Features

  • Page Navigation: UPagination component for page selection
  • Page Size Selector: Dropdown to change items per page
  • Result Summary: Shows "Showing X to Y of Z results"
  • i18n Support: Uses translation keys for labels
  • Loading State: Disables controls when loading
  • Border Top: Visual separator from table content

Usage

Basic Usage

<template>
  <CroutonTablePagination
    v-model:page="currentPage"
    v-model:page-count="itemsPerPage"
    :total-items="totalItems"
    :loading="isLoading"
  />
</template>

<script setup lang="ts">
const currentPage = ref(1)
const itemsPerPage = ref(10)
const totalItems = ref(250)
const isLoading = ref(false)

watch([currentPage, itemsPerPage], () => {
  fetchData()
})
</script>

Custom Page Sizes

<template>
  <CroutonTablePagination
    v-model:page="page"
    v-model:page-count="pageSize"
    :total-items="total"
    :page-sizes="[10, 25, 50, 100]"
  />
</template>

<script setup lang="ts">
const page = ref(1)
const pageSize = ref(25)
const total = ref(1000)
</script>

With Table Integration

<template>
  <div class="space-y-4">
    <CroutonTable
      :rows="paginatedData"
      :columns="columns"
    />
    
    <CroutonTablePagination
      v-model:page="pagination.page"
      v-model:page-count="pagination.pageSize"
      :total-items="data.length"
      :loading="fetching"
    />
  </div>
</template>

<script setup lang="ts">
const data = ref([]) // All data
const pagination = ref({ page: 1, pageSize: 20 })
const fetching = ref(false)

const paginatedData = computed(() => {
  const start = (pagination.value.page - 1) * pagination.value.pageSize
  const end = start + pagination.value.pageSize
  return data.value.slice(start, end)
})

const fetchData = async () => {
  fetching.value = true
  // Fetch data
  fetching.value = false
}
</script>

Server-Side Pagination

<template>
  <div>
    <CroutonTable :rows="serverData" :columns="columns" />
    
    <CroutonTablePagination
      v-model:page="serverPage"
      v-model:page-count="serverPageSize"
      :total-items="serverTotal"
      :loading="serverLoading"
    />
  </div>
</template>

<script setup lang="ts">
const serverPage = ref(1)
const serverPageSize = ref(10)
const serverTotal = ref(0)
const serverLoading = ref(false)
const serverData = ref([])

const fetchPage = async () => {
  serverLoading.value = true
  const response = await $fetch('/api/data', {
    params: {
      page: serverPage.value,
      pageSize: serverPageSize.value
    }
  })
  serverData.value = response.data
  serverTotal.value = response.total
  serverLoading.value = false
}

watch([serverPage, serverPageSize], fetchPage, { immediate: true })
</script>

Result Summary Format

The summary displays:

Rows per page: [10 ▼]  Showing 11 to 20 of 250 results

Calculations:

  • pageFrom: (page - 1) * pageCount + 1
  • pageTo: Math.min(page * pageCount, totalItems)

Example with page=2, pageCount=10, totalItems=95:

  • Shows: "Showing 11 to 20 of 95 results"

Translation Keys

The component uses these translation keys:

  • table.rowsPerPageColon
  • table.rowsPerPage (for select label)
  • table.showing
  • table.to
  • table.of
  • table.results
Sticky Footer: Use mt-auto class on parent container to keep pagination at bottom of available space.


CroutonTableSearch

Debounced search input for filtering table data.

Props

interface TableSearchProps {
  modelValue: string
  placeholder?: string
  debounceMs?: number
}
PropTypeDefaultDescription
modelValuestring-Search query string (v-model)
placeholderstring'Search...'Input placeholder text
debounceMsnumber300Debounce delay in milliseconds

Events

EventPayloadDescription
update:modelValuestringEmitted after debounce when search value changes

Features

  • Debounced Input: Uses VueUse useDebounceFn for performance
  • Search Icon: Built-in magnifying glass icon
  • Max Width: Constrained to max-w-sm (24rem)
  • Configurable Delay: Adjust debounce timing
  • Empty State: Emits empty string when cleared

Usage

Basic Usage

<template>
  <CroutonTableSearch v-model="searchQuery" />
</template>

<script setup lang="ts">
const searchQuery = ref('')

watch(searchQuery, (query) => {
  console.log('Searching for:', query)
  // Fetch filtered data
})
</script>

Custom Placeholder and Debounce

<template>
  <CroutonTableSearch
    v-model="query"
    placeholder="Search products..."
    :debounce-ms="500"
  />
</template>

<script setup lang="ts">
const query = ref('')
// Emits after 500ms of no typing
</script>

Integrated with Table

<template>
  <div class="space-y-4">
    <div class="flex justify-between">
      <CroutonTableSearch
        v-model="searchQuery"
        placeholder="Search users..."
      />
      <UButton color="primary" @click="createUser">
        Create User
      </UButton>
    </div>

    <CroutonTable
      :rows="filteredUsers"
      :columns="columns"
    />
  </div>
</template>

<script setup lang="ts">
const searchQuery = ref('')
const users = ref([])

const filteredUsers = computed(() => {
  if (!searchQuery.value) return users.value
  
  const query = searchQuery.value.toLowerCase()
  return users.value.filter(user =>
    user.name.toLowerCase().includes(query) ||
    user.email.toLowerCase().includes(query)
  )
})
</script>

With Loading State

<template>
  <div class="relative">
    <CroutonTableSearch
      v-model="search"
      placeholder="Type to search..."
    />
    <div
      v-if="searching"
      class="absolute right-2 top-2"
    >
      <UIcon name="i-lucide-loader-2" class="animate-spin" />
    </div>
  </div>
</template>

<script setup lang="ts">
const search = ref('')
const searching = ref(false)

watch(search, async (query) => {
  if (!query) return
  
  searching.value = true
  await fetchSearchResults(query)
  searching.value = false
})
</script>
<template>
  <CroutonTableSearch
    v-model="serverSearch"
    placeholder="Search..."
    :debounce-ms="400"
  />
</template>

<script setup lang="ts">
const serverSearch = ref('')

watch(serverSearch, async (query) => {
  const { data } = await useFetch('/api/search', {
    params: { q: query }
  })
  // Update results
})
</script>

Debounce Behavior

Without debouncing, every keystroke would trigger a search:

User types: "hello"
Without debounce: 5 searches (h, he, hel, hell, hello)
With 300ms debounce: 1 search (hello) - after user stops typing

Example debounce values:

  • 100ms - Very responsive, still reduces load significantly
  • 300ms - Default, good balance
  • 500ms - Slower response, fewer API calls
  • 1000ms - Very slow, minimal API calls
Performance: Debouncing is crucial for server-side searches to avoid excessive API calls. The default 300ms works well for most use cases.