Listing page
A complete list page of the panel: toolbar, filter button, bulk selection bar, sortable table and pagination. The look comes from Acun UI components, the behavior (search, sorting, selection, pagination) from the WithListing trait.
Look
The parts of a list page: acun:list-toolbar (search on the left, actions on the right), the Filters panel opened with acun:filter-button, acun:selection-bar, which shows while something is selected, a table with acun:sortable-th headers and the pagination bar. The live example has no Livewire, so clicks do not reach the server.
<div class="space-y-4">
<acun:list-toolbar>
<acun:search name="list_search" placeholder="Product name or SKU" />
<acun:slot:actions>
<acun:filter-button drawer="catalog-listing-filters" :count="2" />
<acun:button>New Product</acun:button>
</acun:slot:actions>
</acun:list-toolbar>
<acun:selection-bar :count="2" clear-action="clearSelection">
<acun:button variant="white" size="sm">Activate</acun:button>
<acun:button variant="danger" size="sm">Delete</acun:button>
</acun:selection-bar>
<acun:table>
<table class="ui-table">
<thead>
<tr>
<th class="w-10"><acun:checkbox id="list-all" aria-label="Select all" /></th>
<acun:sortable-th field="name" sort-field="name" sort-direction="asc">Product Name</acun:sortable-th>
<acun:sortable-th field="sku" sort-field="name" sort-direction="asc">SKU</acun:sortable-th>
<th class="text-center">Stock</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr><td><acun:checkbox id="list-1" checked /></td><td class="font-medium text-gray-900 dark:text-zinc-100">Wireless headphones</td><td>WH-01</td><td class="text-center tabular-nums">312</td><td><acun:badge variant="success">Active</acun:badge></td></tr>
<tr><td><acun:checkbox id="list-2" checked /></td><td class="font-medium text-gray-900 dark:text-zinc-100">Mechanical keyboard</td><td>MK-04</td><td class="text-center tabular-nums">148</td><td><acun:badge variant="success">Active</acun:badge></td></tr>
<tr><td><acun:checkbox id="list-3" /></td><td class="font-medium text-gray-900 dark:text-zinc-100">USB-C charger</td><td>UC-02</td><td class="text-center tabular-nums">96</td><td><acun:badge variant="danger">Inactive</acun:badge></td></tr>
</tbody>
</table>
</acun:table>
{{ $examplePaginator->links('acun-ui::components.ui.pagination') }}
</div>
<acun:drawer name="catalog-listing-filters" docked title="Filters" compact>
<acun:select name="list_status" :options="['' => 'All Statuses', '1' => 'Active', '0' => 'Inactive']" />
</acun:drawer>
| Prop | Values |
|---|---|
acun:list-toolbar | Default slot: search · actions slot: the buttons on the right |
acun:filter-button | drawer: name of the drawer to open · count: number of active filters · label: text (default Filters, in the app language) |
acun:selection-bar | count · all-matching · total · clear-action |
Livewire component
Add WithListing to the component and write the two abstract methods: defaultSortField() returns the default sort field, currentPageKeys() the row keys of the visible page. Enable sort fields other than the default with sortableFields(); fields not in the list are ignored by sortBy() and the URL. Call fillPageSelection() at the start of the with() method.
use Acun\Ui\Livewire\Concerns\WithListing;
use Acun\Ui\Search\SearchField;
use Acun\Ui\Search\SearchMode;
new class extends Component
{
use WithListing;
public string $status = '';
protected function defaultSortField(): string
{
return 'name';
}
protected function sortableFields(): array
{
return ['name', 'sku'];
}
protected function currentPageKeys(): array
{
return $this->baseQuery()->forPage($this->getPage(), $this->perPage)->pluck('id')->all();
}
public function updatedStatus(): void
{
$this->resetPage();
$this->clearSelection();
}
// A word anywhere in the product name, the SKU from the start (name_search, sku_search columns).
protected function searchFields(): array
{
return [SearchField::make('name', SearchMode::Contains), SearchField::make('sku')];
}
private function baseQuery(): Builder
{
return $this->applySearch(Product::query())
->when($this->status !== '', fn ($q) => $q->where('is_active', $this->status))
->orderBy($this->sortField, $this->sortDirection);
}
public function with(): array
{
$this->fillPageSelection();
return ['products' => $this->baseQuery()->paginate($this->perPage)];
}
};
| Prop | Values |
|---|---|
defaultSortField() | Required: the default sort field |
currentPageKeys() | Required: the row keys of the visible page |
sortableFields() | Sortable fields (default: only defaultSortField) |
perPageOptions() | Page sizes (default: 10 | 25 | 50 | 100) |
Blade template
Bind the parts of the view to the trait's properties: the search to search, the header checkbox to pageSelected and the row checkboxes to selected. acun:sortable-th calls sortBy() when clicked. For pagination, links() is enough; the trait picks the Acun UI pagination as the view.
<acun:list-toolbar>
<acun:search wire:model.live.debounce.400ms="search" placeholder="Product name or SKU" />
<acun:slot:actions>
<acun:filter-button drawer="product-filters" :count="$status !== '' ? 1 : 0" />
</acun:slot:actions>
</acun:list-toolbar>
<acun:selection-bar :count="count($selected)" :all-matching="$allMatchingSelected" :total="$products->total()">
<acun:modal.trigger name="bulk-delete-products"><acun:button variant="danger" size="sm">Delete</acun:button></acun:modal.trigger>
</acun:selection-bar>
<acun:table>
<table class="ui-table">
<thead>
<tr>
<th class="w-10"><acun:checkbox wire:model.live="pageSelected" aria-label="Select all on this page" /></th>
<acun:sortable-th field="name" :sort-field="$sortField" :sort-direction="$sortDirection">Product Name</acun:sortable-th>
<acun:sortable-th field="sku" :sort-field="$sortField" :sort-direction="$sortDirection">SKU</acun:sortable-th>
</tr>
</thead>
<tbody>
@foreach ($products as $product)
<tr wire:key="product-{{ $product->id }}">
<td><acun:checkbox wire:model.live="selected" value="{{ $product->id }}" /></td>
<td>{{ $product->name }}</td>
<td>{{ $product->sku }}</td>
</tr>
@endforeach
</tbody>
</table>
</acun:table>
{{ $products->links() }}
Selecting everything that matches the filters
When the header checkbox is checked, the keys of the visible page are selected and allMatchingSelected turns on; acun:selection-bar then shows the total value as "all matching the filters". If the user unchecks a row, this mode turns off on its own and the selection narrows to the checked rows. The selection is cleared when the search changes; remember to call clearSelection() in your own filters.
<acun:selection-bar :count="25" :all-matching="true" :total="148" clear-action="clearSelection">
<acun:button variant="white" size="sm">Deactivate</acun:button>
</acun:selection-bar>
| Prop | Values |
|---|---|
all-matching | true | false |
total | Total number of records matching the filters |
clear-action | Livewire method that clears the selection (default clearSelection; when null, the button is hidden) |
Search columns
The search runs on a normalized column that the database generates from the source column. It ignores Turkish characters and letter case: "ayse" finds Ayşe, "ismail isik" finds İsmail Işık. Add the column in a migration with searchColumn(); first and last names in separate columns can be combined into one column. The default mode is prefix search, which uses the index; Contains reads every row. % and _ in the term are treated as plain text. Details: the Search guide.
// Migration
Schema::table('customers', function (Blueprint $table) {
$table->searchColumn('tax_number'); // tax_number_search + index
$table->searchColumn(['first_name', 'last_name'], 'full_name_search'); // "Ayşe Yılmaz" is searched as a whole
});
// Component
protected function searchFields(): array
{
return [
SearchField::make('tax_number', SearchMode::Exact),
SearchField::column('full_name_search', SearchMode::Contains),
];
}
| Prop | Values |
|---|---|
SearchMode | Prefix (default, indexed) | Exact | Contains (scan) | FullText (MySQL FULLTEXT) |
SearchField::make(source, mode, column) | The source_search column; in a joined table, qualify it like users.email |
SearchField::column(column, mode) | A column generated from several sources |
Props and slots
The values the component accepts.
| Prop | Default | Description |
|---|---|---|
$search | '' | Search term · URL: ?q= · when it changes, the page resets to the first and the selection is cleared |
$sortField | '' | URL: ?sort= · a value that is not allowed becomes defaultSortField() |
$sortDirection | asc | asc · desc · URL: ?direction= |
$perPage | 25 | A value outside perPageOptions() falls back to the first option |
$selected | [] | Keys of the selected rows (wire:model.live="selected") |
$pageSelected | false | Header checkbox; when true, currentPageKeys() are selected |
$allMatchingSelected | false | Everything matching the filters is selected; becomes false when a row is unchecked |
defaultSortField() | — | Required (abstract) |
currentPageKeys() | — | Required (abstract) |
sortableFields() | [defaultSortField()] | Sortable fields |
perPageOptions() | [10, 25, 50, 100] | Allowed page sizes |
sortBy($field) | — | Flips the direction on the same field, starts with asc on a new field; resets to the first page |
clearSelection() | — | Resets selected, pageSelected and allMatchingSelected |
fillPageSelection() | — | Called at the start of with(); while "all selected" is on, adds the keys of the new page |
applySearch($q, $term = null, $fields = null) | — | Searches the searchFields() fields with $search; insensitive to Turkish characters and letter case |
searchFields() | — | Search fields (a list of SearchField) |
paginationView() | acun-ui::components.ui.pagination | links() uses this view |