DEV Community

Cover image for Wiretables: tables for Laravel and Livewire written in Blade
Eduardo Lázaro
Eduardo Lázaro

Posted on

Wiretables: tables for Laravel and Livewire written in Blade

Table packages for Livewire usually define the columns in PHP arrays, which is comfortable until one cell needs something the configuration did not plan for.

Wiretables goes the other way: you write the table in Blade, cell by cell, and the package brings the pieces, their look and the behaviour every list repeats. Columns that hide or stack into cards on a phone, header sorting kept in the URL, load more, a row actions menu nothing cuts off, and the 11 themes of the wire* family. It is pure CSS, so it does not depend on Tailwind or Bootstrap.

Install

It is a regular Laravel package. Require it with Composer and publish the CSS:

composer require edulazaro/wiretables
php artisan vendor:publish --tag=wiretables-assets
Enter fullscreen mode Exit fullscreen mode

Load the core and, if you want one, a theme. With Vite, straight from vendor:

@import "../../vendor/edulazaro/wiretables/resources/css/wiretables-core.css";
@import "../../vendor/edulazaro/wiretables/resources/css/themes/studio.css";
Enter fullscreen mode Exit fullscreen mode

Each theme is its own file, so adding themes to the package never makes your CSS bigger. The row menu uses Alpine and its anchor plugin, both bundled with Livewire.

A table

The table is plain Blade: a head slot with the columns, then one row per record:

<x-wiretable>
    <x-slot:head>
        <x-wiretable.th>Event</x-wiretable.th>
        <x-wiretable.th hide="sm" shrink>Status</x-wiretable.th>
        <x-wiretable.th hide="md" shrink>Date</x-wiretable.th>
        <x-wiretable.th actions />
    </x-slot:head>

    @forelse ($projects as $project)
        <x-wiretable.row wire:key="project-{{ $project->id }}">
            <x-wiretable.td>
                <x-wiretable.primary :title="$project->name" :subtitle="$project->email" :href="route('projects.show', $project)">
                    <p class="md:hidden">{{ $project->status }} · {{ $project->date }}</p>
                </x-wiretable.primary>
            </x-wiretable.td>
            <x-wiretable.td hide="sm" shrink>{{ $project->status }}</x-wiretable.td>
            <x-wiretable.td hide="md" shrink>{{ $project->date }}</x-wiretable.td>
            <x-wiretable.td actions>
                <x-wiretable.menu :label="'Actions of '.$project->name">
                    <x-wiretable.menu-item :href="route('projects.show', $project)">Open</x-wiretable.menu-item>
                    <x-wiretable.menu-separator />
                    <x-wiretable.menu-item wire:click="archive({{ $project->id }})" danger>Archive…</x-wiretable.menu-item>
                </x-wiretable.menu>
            </x-wiretable.td>
        </x-wiretable.row>
    @empty
        <x-wiretable.empty colspan="4">Nothing yet.</x-wiretable.empty>
    @endforelse
</x-wiretable>
Enter fullscreen mode Exit fullscreen mode

The first column holds the record and takes the room left; shrink fits the rest to their content. align="right" lines up figures and amounts.

On a phone

A column with hide="md" shows from that breakpoint up, with the same widths as Tailwind's sm, md, lg and xl. Nothing scrolls sideways: what a phone cannot show, the first cell repeats in its slot, as the md:hidden line above does.

When every value matters on a phone too, stack the rows into cards instead:

<x-wiretable stack="md" expandable>
    …
    <x-wiretable.td hide="md" label="Price" align="right">{{ $property->price }}</x-wiretable.td>
</x-wiretable>
Enter fullscreen mode Exit fullscreen mode

Below md each row becomes a card and each cell a line with its label above it. With expandable, the cells with hide fold behind a button in the actions cell; compact keeps the main cells and the actions on one line. On a desktop the table does not change.

Sorting

A header becomes a sort button when you give it a key and the component's current order:

<x-wiretable.th sortable="date" :sort="$sort" :direction="$direction">Date</x-wiretable.th>
Enter fullscreen mode Exit fullscreen mode

The Livewire side is a trait. It keeps sort and direction in the URL, cycles ascending, descending and back to the list's own order, and goes back to the first page:

use EduLazaro\Wiretables\Concerns\WithSorting;

class Projects extends Component
{
    use WithPagination, WithSorting;

    protected function sortable(): array
    {
        return ['date', 'received'];
    }

    public function render()
    {
        $column = ['date' => 'starts_on', 'received' => 'created_at'][$this->sort] ?? null;

        return view('livewire.projects', [
            'projects' => Project::query()
                ->when($column, fn ($query) => $query->orderBy($column, $this->direction))
                ->paginate(25),
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Only the keys sortable() returns are accepted, because the client can send any string and a column name must never come from it.

Load more

For a list that grows instead of paging, WithLoadMore and its button do the work:

use EduLazaro\Wiretables\Concerns\WithLoadMore;

$projects = $this->loadMoreFrom(Project::query()->latest());
Enter fullscreen mode Exit fullscreen mode
<x-wiretable>…</x-wiretable>
<x-wiretable.load-more :show="$hasMore" />
Enter fullscreen mode Exit fullscreen mode

Each read asks for everything up to the current page from the start rather than skipping what was loaded, so a record added or removed meanwhile is never repeated or lost. The page count is locked, so the client cannot ask for a thousand rows at once, and any filter or new order starts again from the first page.

The row menu

Row actions live in x-wiretable.menu, a "⋯" that fits on any screen where a row of buttons would not:

  • It is moved to <body> and anchored to its button, so a scrolling table, a card or a modal never cuts it off.
  • It flips upwards near the bottom of the screen and closes on a choice, a click outside or Escape.
  • menu-item is a link with href or a button otherwise, with an icon in front and danger for what cannot be undone.

A few more options

The table adapts to where it sits with a handful of attributes:

  • rows="separated" draws each row as a card of its own, with a little room between them.
  • flush drops the frame for a table inside a card, such as a dashboard panel.
  • The footer slot holds pagination, totals or the load more button.

Themes

The theme is one attribute on <html>, shared with the rest of the wire* family:

<html data-wire-theme="studio">
Enter fullscreen mode Exit fullscreen mode

There are 11 themes, each with its dark mode through data-wire-theme-mode="dark" or a .dark ancestor. For your own look, override the --wire-* variables for the whole family or --wtb-* for the table:

:root {
    --wtb-text: #1e2a36;
    --wtb-line: #e3e6e1;
    --wtb-row-hover: #f9faf8;
    --wtb-radius: 6px;
}
Enter fullscreen mode Exit fullscreen mode

Lists without columns

When each record reads as a card rather than a row of columns, use Wirelist, built on this package with the same look, menu and load more.

Links

👉 Source: Wiretables on GitHub
👉 Packagist: Wiretables on Packagist
👉 Documentation: Wiretables Documentation

Top comments (0)