Skip to content

Rendering and slots ​

Use the default renderer for table structure and slots for targeted application content. The server resource remains authoritative even when a cell or control is replaced.

Event typing ​

Type the resource as TableResource<YourRow> to infer row-click and row-key arguments. row-click receives (item, column) and column may be null.

EventArguments
custom-actionaction, keys, onFinish, selection
action-successaction, keys, selection
action-erroraction, keys, error: unknown, selection
action-queued, action-progressaction, QueuedActionStatus, selection
export-successTableExport
export-queuedTableExport, QueuedExportStatus
export-errorTableExport, Error

Use the public package's TableAction, TableKey, TableSelection, QueuedActionStatus, QueuedExportStatus and TableExport types for named handlers. Narrow action errors before reading properties; export errors already use Error. The custom-action completion callback takes no arguments.

Stricter slot types can reveal assumptions previously hidden by any: check nullable action rows and image fallbacks, and narrow cell/filter values. For programmatic mounting, specify the component generic when inference is lost, for example mount(DataTable<Topic>, ...) in Vue Test Utils.

Custom cells ​

cell(...) slots infer item from the resource's row type. Their column is TableColumn and their value is unknown, since PHP mapping can change a cell's value independently of the row attribute. Narrow value before using it as a string or number. header(...) slots expose a typed column (its title is column.header). Both retain the shared table/actions/exports/views scope. action(...) exposes item: T | null because bulk actions have no row, selectedItems: T[], selection/count, and a zero-argument execute() callback. Check item before reading row fields. filter(...) exposes optional applied state, an unknown draft value, update(value?), close() and setDisplayValue(string | null). Shared scope remains available in both. summary(...) exposes value: unknown, formatted: string, its column and optional summary definition. image(...) exposes image metadata, while image-fallback(...) allows that metadata to be null. Layout slots such as topbar, thead, tbody, footer, loading and emptyState receive the shared scope. confirmation adds the pending action, and queuedAction adds a nullable status (it can also render when an error exists without a status). Unknown extension slot names retain a permissive fallback.

vue
<DataTable :resource="topics">
    <template #cell(name)="{ item }">
        <strong>{{ item.name }}</strong>
    </template>

    <template #emptyState>
        <p class="py-10 text-center text-muted-foreground">
            No topics found.
        </p>
    </template>
</DataTable>

Runnable custom cell and action recipe ​

Use the Topic model/schema from getting started. This recipe renders names in bold and provides a row Preview button that updates a status message on the page. It performs no server mutation.

The consumer table below uses its fixture namespace. In your application use namespace App\Tables; and import App\Models\Topic:

php
<?php

namespace Musing\InertiaTable\Tests\Consumer;

use Illuminate\Database\Eloquent\Builder;
use Musing\InertiaTable\Actions\Action;
use Musing\InertiaTable\Columns\ActionColumn;
use Musing\InertiaTable\Columns\TextColumn;
use Musing\InertiaTable\Table;

final class SlotsTable extends Table
{
    protected ?string $name = 'topics';

    public function query(): Builder
    {
        return Topic::query();
    }

    public function columns(): array
    {
        return [
            TextColumn::make('name')->searchable()->sortable(),
            ActionColumn::new(),
        ];
    }

    public function actions(): array
    {
        return [Action::make('preview', 'Preview')->row()];
    }
}

Keep ActionColumn::new() in columns(): declaring a row action alone does not add a place to render its button.

Return it under the topics prop (matching the declared table name):

php
use App\Tables\SlotsTable;

return inertia('Topics/Slots', ['topics' => SlotsTable::make()]);

Register the following component as Topics/Slots in your page resolver:

vue
<script setup lang="ts">
import { ref } from "vue";
import {
    DataTable,
    type TableResource,
    type TableAction,
    type TableKey,
} from "@musing/inertia-table-vue";

type Topic = { id: number; name: string };
const props = defineProps<{ topics: TableResource<Topic> }>();
const preview = ref("");

function previewAction(
    action: TableAction,
    keys: TableKey[],
    onFinish: () => void,
) {
    try {
        if (action.key !== "preview") return;
        const topic = props.topics.results.data.find(
            (row) => row.id === keys[0],
        );
        preview.value = topic
            ? `Preview: ${topic.name}`
            : "Topic is no longer on this page.";
    } finally {
        onFinish();
    }
}
</script>

<template>
    <main>
        <h1>Custom cells and actions</h1>
        <DataTable :resource="topics" @custom-action="previewAction">
            <template #cell(name)="{ item }">
                <strong>{{ item.name }}</strong>
            </template>
            <template #action(preview)="{ item, execute }">
                <button v-if="item" type="button" @click="execute()">
                    Preview {{ item.name }}
                </button>
            </template>
        </DataTable>
        <p role="status">{{ preview }}</p>
    </main>
</template>

An action without an endpoint or managed handler emits custom-action. The slot calls execute() to enter the package action flow; the event handler calls onFinish() in finally so completion also occurs on an early return or error. For asynchronous work, await it inside that try block. The host owns error presentation for custom work. Any server endpoint must still authorize the operation; a visible slot is not an authorization check.

From the package checkout run npm run test:consumer, then node tests/Consumer/serve.mjs. Open /slots using the printed origin and click Preview Alpha: the status should become “Preview: Alpha”. Click Preview Beta next to check that the previous action finished. Repeat with ?csr=1. The automated consumer verifies public types, CSR/SSR builds, and server HTML containing the custom cell and button. Clicking and hydration are manual checks.

Slot inventory ​

Layout slots:

  • topbar
  • beforeSearch and afterSearch
  • beforeActions and afterActions
  • filters
  • table, thead, and tbody
  • summaryFooter
  • footer
  • loading
  • emptyState
  • confirmation
  • queuedAction

Attribute slots:

  • cell(<attribute>)
  • header(<attribute>)
  • summary(<attribute>)
  • filter(<attribute>)
  • image(<attribute>)
  • image-fallback(<attribute>)
  • action(<key>)

Use attribute slots when only one definition needs custom rendering. Replacing the structural table slots makes the application responsible for accessibility and layout behavior inside that region.

Application-owned filter control ​

Use filter(<attribute>) for a non-Eloquent source, option creation, or a custom picker. First declare the stored value on the server:

php
NumericFilter::make('source_id', 'Source')->withoutClause();

Then render the control:

vue
<DataTable :resource="quotes">
    <template
        #filter(source_id)="{
            value,
            update,
            setDisplayValue,
            close,
        }"
    >
        <AsyncSourceSelect
            :model-value="value"
            @select="({ value: nextValue, label }) => {
                update(nextValue);
                setDisplayValue(label);
                close();
            }"
        />
    </template>
</DataTable>

The package owns the value and URL state. The application owns loading, search, pagination, option creation, and the external endpoint.

Icon resolver ​

Icon names are library-agnostic. Register a resolver once:

ts
import { Pencil, Trash2 } from "@lucide/vue";
import { setIconResolver } from "@musing/inertia-table-vue";

setIconResolver((name) => ({ Pencil, Trash2 })[name]);

The resolver receives both the icon name and its context. A table may instead provide its own iconResolver prop.

Row identity ​

Laravel-generated resources include stable Eloquent key metadata. For an application-owned resource, provide the identity explicitly:

vue
<DataTable
    :resource="topics"
    :row-key="(topic) => topic.uuid"
/>

The headless equivalent is useActions(table, { rowKey: (topic) => topic.uuid }).

For a fully application-owned renderer, see the headless API.

Released under the MIT License.