K

Table

Row Grouping

Rows gathered under a header by a column's value, with per-group subtotals on top of the usual footer.

On this page

Group rows by a column value: the table orders records so groups stay contiguous, renders a header row for each group, and adds per-group subtotal rows for every column with a summary — on top of the usual grand-total footer.

Quick Start

use NyonCode\WireTable\Columns\TextColumn;
use NyonCode\WireTable\Table;
 
public function table(Table $table): Table
{
return $table
->model(Invoice::class)
->columns([
TextColumn::make('number')->label('Invoice'),
TextColumn::make('customer')->label('Customer'),
TextColumn::make('total')
->suffix(' Kč')
->summaryDecimals(0)
->summarizeSum('Sum'),
])
->groupBy('customer');
}
┌──────────────────────────────┐
│ Acme │ ← group header
├────────────┬─────────────────┤
│ INV-2 │ 250 Kč │
│ INV-4 │ 25 Kč │
│ Sum: │ 275 Kč │ ← group subtotal
├──────────────────────────────┤
│ Beta │
│ INV-1 │ 100 Kč │
│ INV-3 │ 50 Kč │
│ Sum: │ 150 Kč │
├────────────┼─────────────────┤
│ Sum: │ 425 Kč │ ← grand total footer
└────────────┴─────────────────┘

Configuration

Method Effect
groupBy(string $column) Group rows by a direct column on the model
groupLabel(string|Closure) Customize the group header label
groupSummaries(bool) Toggle per-group subtotal rows (default on)
collapsibleGroups(bool) Let a user fold a group away (default off)

Group Labels

The header shows the raw group value by default. A string label becomes a prefix; a closure receives the value and the group's first record:

->groupBy('customer')->groupLabel('Customer') // "Customer: Acme"
->groupBy('status')->groupLabel(fn ($value) => match ($value) {
'paid' => '✓ Paid',
'pending' => '⏳ Pending',
default => ucfirst((string) $value),
})

Empty and null group values render as .

Sorting

Grouping prepends an ascending order on the group column, so any other sort — the configured defaultSort() or a user's header click — applies within each group. Sorting by the group column itself takes over completely: the user's direction then controls group order (and groups stay contiguous, since sorting by the group column orders groups by definition).

Subtotals

Group subtotal rows appear automatically for every column with a summary; all aggregate types and formatting apply. Subtotals are computed in memory from the group's rows on the current page.

TextColumn::make('total')
->summaryDecimals(0)
->summarizeSum('Sum') // → group subtotal row + grand total footer
->summarizeAvg('Average'), // each summary gets its own subtotal row

Disable the subtotal rows (keeping headers and the footer) with ->groupSummaries(false).

Collapsible Groups

collapsibleGroups() puts a chevron on every group header. Clicking it folds that group's rows away and leaves the header and the group's subtotal on screen:

->groupBy('customer')
->collapsibleGroups();

A collapsed group's rows are not rendered at all — not hidden with CSS, not moved off screen. That is the whole point rather than an implementation detail: several table behaviours read their rows straight out of the DOM (keyboard navigation, range selection, the fill handle, live cell sync), and they keep working through a collapse because the list they walk stays consistent with what is on screen. It is also why this framework offers no virtual scrolling: rendering only the viewport would need a parallel path for each of those four, and three of them fail silently — a fill that writes nothing, a range that skips rows.

What stays visible is what a folded group is worth reading: its header, and its subtotal row. Twenty invoices fold away and the customer's total is still there.

The collapsed set is keyed by the group's own value, not by the rows in it, so a group stays folded when its contents change — a filter that swaps every row in Overdue leaves Overdue folded, which is what the user asked for. It lives in the table state under rows.collapsedGroups, which means it survives a Livewire round trip and is one of the things a saved view carries.

use Livewire\Component;
use NyonCode\WireTable\Columns\TextColumn;
use NyonCode\WireTable\Concerns\WithTable;
use NyonCode\WireTable\Table;
 
class ListInvoices extends Component
{
use WithTable;
 
public function table(Table $table): Table
{
return $table
->model(Invoice::class)
->perPage(100)
->columns([
TextColumn::make('number')->label('Invoice'),
TextColumn::make('issued_at')->date(),
TextColumn::make('total')
->summaryDecimals(0)
->summarizeSum('Sum'),
])
->groupBy('customer')
->collapsibleGroups();
}
}

Collapsing is only meaningful on a grouped table: collapsibleGroups() without groupBy() renders no toggles rather than erroring, and hasCollapsibleGroups() reports false. Driving it from a custom view is two methods on the component — toggleGroup(string $group) and isGroupCollapsed(string $group) — both keyed by the same group value the header shows.

Limits

  • Direct columns only. groupBy('customer.name') throws — grouping must order the query by the group column, which a relationship path can't do without a join. Expose the related value on the query (join + select alias) and group by the alias instead.
  • Pagination splits groups. A group crossing a page boundary shows a partial subtotal on each page. For strict accounting reports, disable pagination (->paginated(false)) or raise perPage().
  • Desktop table layout. Group headers/subtotals render in the standard table layout; the stacked mobile card layout ignores grouping.
  • Exports contain data rows and grand totals, not group subtotal rows.