Vue Component
Import
<script setup lang="ts">
import {TableBuilder} from '@/components'
import type {TableData} from '@/types/table-builder'
defineProps<{ table: TableData }>()
</script>
<template>
<TableBuilder :table="table"/>
</template>
The component is exported from the package's resources/js/components/ directory, which is aliased via @.
Props
| Prop | Type | Required | Description |
|---|---|---|---|
table |
TableData |
Yes | The serialized table data from the PHP TableBuilder |
name |
string |
No | Overrides the table name used to namespace query params ({name}_page, {name}_perPage). Usually unnecessary: the name is read from the serialized table payload (derived from the table class), so it only needs setting for inline tables where you called ->name() manually |
only |
string[] |
No | Inertia prop name(s) to reload on pagination/per-page changes, enables partial reloads so only this table's data is fetched |
paginationPosition |
'top'|'bottom'|'both' |
No | Where the pagination controls render. Defaults to bottom; top puts a compact version next to the column selector |
transport |
'inertia'|'http' |
No | How the table fetches its rows. Defaults to inertia. See HTTP Transport |
source |
string |
No | Url the http transport fetches from. Required with transport="http" |
fetcher |
TableFetcher |
No | Replaces the built-in request of the http transport |
adapter |
TableAdapter |
No | Maps a non-standard response body onto {data, pagination} |
reorderable |
boolean |
No | Adds a drag handle in front of every row. See Reordering |
rowClass |
(row, index) => string | string[] | Record<string, boolean> |
No | Extra classes per row, for status colours and similar. Merged with the classes of $table->rowClass(), the prop last so it wins a conflicting Tailwind class. See Row classes |
emptyState |
EmptyState |
No | Texts for an empty table, overriding the ones from the Table class. See Empty State |
rowActionIcons |
Record<string, Component> |
No | Icon components per icon name for the row actions of this table, over the ones from registerRowActionIcons(). See Row Actions |
confirm |
(message, details?) => boolean | Promise<boolean> |
No | Asks the confirmation of a row or bulk action. Defaults to window.confirm(). A row action passes its text as resolved for that row, so a confirm closure in PHP can name the row. A bulk action passes details with its text, confirmButton and cancelButton. See Row Actions and Bulk Actions |
Features Rendered
The component renders all of the following automatically based on what the PHP builder provides:
| Feature | Rendered when |
|---|---|
| Column headers with sort indicators | columns exist |
| Column visibility dropdown | any column has can_be_hidden: true and columnToggle is not false |
| Filter dropdowns | filters array is non-empty |
| Search inputs | searchInputs map is non-empty |
| Pagination controls | pagination object is present |
| Per-page selector | perPageOptions has more than one option |
| Bulk action toolbar | bulkActions array is non-empty |
| Row checkboxes | bulk actions present |
| Clickable rows | rowLinks array is non-empty, or a row-click listener is bound |
| Drag handles | reorderable is set |
| Totals footer | totals is set and the table has rows |
| Export button | export.url is set |
| Reset button | any filter/search/sort is active |
User Interactions
With the default inertia transport every interaction makes an Inertia visit with preserveState: true and
preserveScroll: true so the page does not fully reload. With the http transport the same interactions change the
in-memory query and refetch from source instead; the url is left alone. See HTTP Transport.
| Interaction | Query Param Changed |
|---|---|
| Click sortable column header | sort=column or sort=-column |
| Toggle column visibility | columns[]=key list |
| Change filter dropdown | filter[key]=value |
| Type in search input | filter[key]=term (debounced) |
| Change page | {name}_page=N (or page=N) |
| Change per-page | {name}_perPage=N (or perPage=N) |
| Select rows + run bulk action | POST to signed URL |
| Shift-click a row checkbox | none: selects or unselects the range since the last clicked row |
Search Debounce
Search inputs are debounced to avoid firing on every keystroke. The default is 350ms. Change it globally:
TableBuilder::defaultSearchDebounce(500); // ms
Dark Mode
The component respects the dark class on the <html> element and uses Tailwind's dark mode utilities throughout.
Customizing Appearance
Global cell/header classes
$table->class(cell: 'py-3 text-sm', head: 'bg-muted font-semibold');
Per-column classes
->column('id', 'ID', classes: 'w-16 tabular-nums')
->column('id', 'ID', classes: ['w-16' => true, 'hidden' => $compact])
The resolved classes arrive in the class key of the column payload.
CSS Variables
The package uses shadcn/ui-style CSS variables. Override in your app.css:
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
/* ... */
}
Row classes
Style rows from the table class with rowClass(). The callback gets the original row (the model, not the formatted
array) and returns a string, a list of classes or a class => bool array:
$table->rowClass(fn (Message $message): array => [
'font-semibold' => ! $message->is_seen,
'text-muted-foreground' => $message->is_archived,
]);
The result travels as __class on each row, only for rows with a class and only when rowClass() is set. A row that
already has a __class value throws a LogicException; alias that column in the query. The component merges it
with the rowClass prop through tailwind-merge, prop last.
Auto-refresh
$table->poll(30) sends poll: 30, and the component reloads the rows every 30 seconds: with the inertia
transport through router.reload(), with the http transport by a silent refetch of the current query. Sorting, search, filters, the page and the selection stay as they are.
It pauses while rows are selected, while a confirmation or choice dialog is open, while an action or request runs
and while the tab is hidden. A tick missed while the tab was hidden runs as soon as it is visible again. Unmounting
stops the timer. Without poll() the payload has no poll key and nothing is scheduled.
With the inertia transport, always pass only with the name of the table prop:
<TableBuilder :table="messages" :only="['messages']" />
Without it every tick reloads all props of the page, shared props included, and the component warns once in development.
Translations
The component reads its texts from the shared Inertia prop vue_table_builder_table_translations. useTranslations is
exported from the package entry resources/js/index.ts, here aliased as @table-builder:
import {useTranslations} from '@table-builder'
const {t} = useTranslations('vue_mailbox_translations', {group: 'mailbox'})
t('mailbox::mailbox.compose.title')
t('compose.title')
Share the bundle from your own service provider, for the current locale:
Inertia::share('vue_mailbox_translations', fn () => trans('mailbox::mailbox'));
A vendor:: prefix is always stripped. group strips the file name the bundle was shared from; it defaults to
table for the table builder's own bundle and to nothing for any other.
Slots
| Slot | Scope | Renders |
|---|---|---|
cell-{key} |
row, value, index, column |
Replaces the cell body of the column named {key} |
actions |
row, index, actions, run |
A trailing column on every row, right aligned; clicks do not follow the row link. Replaces the buttons of Row Actions; run ignores clicks while another row action runs |
empty |
filtered, title, description, reset |
The content of the row shown when there are no rows. See Empty State |
toolbar |
- | Controls next to the column selector above the table |
row-after |
row, index, columns, colspan |
Extra rows under each row. See Nested Rows |
total-{key} |
value, column, totals |
Replaces the total of the column named {key} in the footer. See Totals |
<TableBuilder :table="table">
<template #cell-status="{value}">
<Badge :variant="value === 'active' ? 'default' : 'secondary'">{{ value }}</Badge>
</template>
<template #actions="{row}">
<Button size="sm" @click="edit(row)">Edit</Button>
</template>
<template #toolbar>
<Button variant="outline" @click="openSettings">Settings</Button>
</template>
</TableBuilder>
Without a cell-{key} slot the value is rendered with v-html. The PHP builder escapes string
values before they are serialized, so that is safe for tables driven by the builder. It is not safe
for rows fetched from an API with the http transport: escape those yourself, or render them
through a cell-{key} slot.
Events
| Event | Payload | Fired when |
|---|---|---|
reorder |
{from, to, row} |
A row is dropped on another row of a reorderable table |
row-click |
{row, index, key, event} |
A row without a row link is clicked; key is the row's row key value |
Reordering Rows
reorderable() in the Table class, or the reorderable prop, puts a drag handle in front of every
row and emits reorder with {from, to, row} after a drop. See Reordering for
storing the order on the server, how ties are broken and handling the event yourself.
Clicking Rows
A row with a row link from the PHP builder follows that link. Every other row emits row-click, and
gets the pointer cursor as soon as a listener is bound, so an http table can open a record without
a server-side row link. Clicks on the drag handle, the selection checkbox, the actions slot and
columns marked clickable: false never reach it.
<TableBuilder :table="table" @row-click="({row}) => open(row)"/>
Nested Rows
The row-after slot renders below each row, for option lines or grouped sub-rows. colspan is the
full width of a row including the handle, selection and actions columns, and columns are the
visible columns. TableRow and TableCell are exported so those rows look like the rest of the
table.
<script setup lang="ts">
import {TableBuilder, TableCell, TableRow} from '@/components'
</script>
<template>
<TableBuilder :table="table">
<template #row-after="{row, colspan}">
<TableRow v-for="option in row.options" :key="option.id" class="bg-muted/30">
<TableCell :colspan="colspan" class="pl-10">{{ option.name }}</TableCell>
</TableRow>
</template>
</TableBuilder>
</template>
Exposed Methods
<script setup lang="ts">
import {useTemplateRef} from 'vue'
const table = useTemplateRef('table')
function refresh() {
table.value?.reload()
}
</script>
<template>
<TableBuilder ref="table" :table="table"/>
</template>
| Method | Returns | Description |
|---|---|---|
reload() |
void |
Refetches the rows (http) or reloads the Inertia props (inertia) |
currentQuery() |
TableQuery |
The active sort, filters, search, page and per-page |
exportUrl() |
string | null |
The signed export url with the current state appended. See Export |
TypeScript
Import types for use in your pages:
import type {TableData, Column, Filter, BulkAction, PaginationData} from '@/types/table-builder'
See TypeScript Types for the full interface reference.
