Content Index
- Creating the Datatable in Laravel Livewire
- Creating the Datatable in Laravel Livewire
- Define Model
- Define trait for search fields using queryString
- Process data for listing, render function
- Full project code
- View code
- Filters on a list or table in Laravel Livewire
- Filters in Laravel Livewire
- Paged listing with Volt
A table of type datatable, is the term that we use to indicate a table in basic HTML with the data with the CRUD type operations for each one of these data; this table; It is a table that allows us to define a set of useful options for our user such as pagination, search fields, column ordering, and of course, options to edit, delete, and all the others you need; In this entry we are only going to create the table, with the corresponding options for it, using tailwind.css for our interface, although you can use anything else like native CSS or even the tables we have for Bootstrap.
At this point, we already know how to use component properties in Livewire, which will be a key piece to convert our table or list into a datatable.
A DataTable can store data of any type, including text, numbers, dates, images, among other types. It is commonly used in web applications to display data in a table in an ordered and structured way. For example, if there is information from a list of publications, in this supposed list the options to delete, create, edit, see the detail, and filter the post may appear.
In the end, a Datatable is a table on steroids in which you define multiple management, filter, and sort options.
In Laravel Livewire, thanks to all the reactive behavior and communication with the server directly from the client, we can create these types of tables very easily.
Creating the Datatable in Laravel Livewire
Creating the Datatable in Laravel Livewire
As we mentioned before, there is some management component in the tables, therefore we have to define the model, the view and the component.
Define Model
First things first, and we need a data source, our user model, which is the one that comes free with Laravel Livewire, is quite convenient for us, but you can use any other; in our model of a lifetime for users with the traits that we have by default:
class User extends Authenticatable #implements MustVerifyEmail
{
use HasApiTokens;
use HasFactory;
use HasProfilePhoto;
use HasTeams;
use Notifiable;
use TwoFactorAuthenticatable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password'
];
}Define trait for search fields using queryString
Now we go to our component, for that we are going to use a Livewire component that we can create with:
php artisan make:livewire UserListAnd remember that with this, 3 elements are created, our Server Side Component and our view; now with the component created, we have to define some characteristics to be able to use the search; we use the property called queryString, to indicate which properties we are going to use for the search; This is useful because if our user refreshes the page, the data that the user is typing is saved in the application's URL and the search would be maintained:
use WithPagination;
protected $queryString = ['name'];
public $name = "";
public $sortColumn = "id";
public $sortDirection = "asc";
public $columns = [
'id',
'name',
'email'
];
public function sort($column)
{
$this->sortColumn = $column;
$this->sortDirection = $this->sortDirection == 'asc' ? 'desc' : 'asc';
}For the rest, we define some more properties to indicate the columns that we are going to use, their names and also a function and properties that will allow us to search by columns, and the type of ordering, ascending or descending.
Process data for listing, render function
Now, the heart of our component, in which we build the query or the query to the database, as well as map the properties that we want to use to filter our data, which in this case is the name/name; otherwise we use pagination and return to the view we want to use:
public function render()
{
$users = User::orderBy($this->sortColumn, $this->sortDirection);
if ($this->name) {
$users->where('name', 'like', '%' . $this->name . '%')
->orWhere('email', 'like', '%' . $this->name . '%');
}
$users = $users->paginate(10);
return view('livewire.users-list', ['users' => $users]); //->layout('layouts.app');
}Full project code
Finally, here you can see the source code of the component used previously, in which we have the columns, as well as the columns to indicate the filter, ordering, in the render function, we indicate the view as well as apply the corresponding filters, remember that each of these properties used in the render function is used at the view level as models in the fields, thanks to the communication between the backend and the frontend, the communication is done directly and this is the secret and most important point of this project:
namespace App\Http\Livewire;
use App\Models\User;
//use Illuminate\View\Component;
use Livewire\Component;
use Livewire\WithPagination;
class UsersList extends Component
{
use WithPagination;
//public $users;
public $columns = [
'id',
'name',
'email'
];
public $name = "";
public $sortColumn = "id";
public $sortDirection = "asc";
protected $queryString = ['name'];
public function render()
{
$users = User::orderBy($this->sortColumn, $this->sortDirection);
if ($this->name) {
$users->where('name', 'like', '%' . $this->name . '%')
->orWhere('email', 'like', '%' . $this->name . '%');
}
$users = $users->paginate(10);
return view('livewire.users-list', ['users' => $users]); //->layout('layouts.app');
}
public function sort($column)
{
$this->sortColumn = $column;
$this->sortDirection = $this->sortDirection == 'asc' ? 'desc' : 'asc';
}
public function cleanFilter()
{
$this->name = "";
}
public function delete(User $user)
{
$user->delete();
}
}View code
The view, it's really simple; As we mentioned, we are using Tailwind, we have a block of inputs referencing the properties of the previous model through the wire:model, which is to define the fields of the form to filter and then we have a block for the table, which already has all the logic implemented in the component from Laravel, we are dedicated to painting the data and columns without any condition:
<div class="flex mt-1 ">
<x-jet-input wire:model="name" class="block w-full" />
<x-jet-secondary-button class="ml-2" wire:click="cleanFilter">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
</x-jet-secondary-button>
</div>
<table class="table-auto w-full">
<thead>
<tr>
@foreach ($columns as $c)
<th class="p-3" wire:click="sort('{{ $c }}')">
<button>
{{ $c }}
@if ($sortColumn == $c)
@if ($sortDirection == 'asc')
↑
@else
↓
@endif
@endif
</button>
</th>
@endforeach
<th class="p-3">Acciones</th>
</tr>
</thead>
<tbody>
@forelse ($users as $u)
<tr>
<td class="border p-3">{{ $u->id }}</td>
<td class="border p-3">{{ $u->name }}</td>
<td class="border p-3">{{ $u->email }}</td>
<td class="border p-3 flex justify-center">
<x-a class="p-1 bg-blue-600 mr-1" href="{{ route('user.edit', $u) }}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</x-a>
<x-jet-danger-button class="p-sm-button" wire:click="delete({{ $u->id }})">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</x-jet-danger-button>
</td>
</tr>
@empty
<tr>
<td colspan="3">
<p>No hay registros</p>
</td>
</tr>
@endforelse
</tbody>
</table>
<br>
{{ $users->links() }}Important points is to note the call to the functions through the click events that allow us to call functions of the component from the view, such as delete or order/sort to perform the ordering.
We are going to work on column ordering individually and ascending/descending for each column that we have in the list.
The scheme that we are going to follow, in order to be able to easily order all the columns of a list, is defining the columns that we are going to show in the list at the level of a property in the Livewire component; with this, we can easily indicate the click events based on the structure proposed in this book:
app/Http/Livewire/Dashboard/Post/Index.php
class Index extends Component
{
use WithPagination;
// ***
// order
public $sortColumn = 'id';
public $sortDirection = 'desc';
public $columns = [
'id' => "Id",
'title' => "Title",
'date' => "Date ",
'description' => "Description",
'posted' => "Posted",
'type' => "Type",
'category_id' => "Category",
];
public function render()
{
//$this->confirmingDeletePost = true;
$posts = Post::orderBy($this->sortColumn,$this->sortDirection);
if ($this->search)
// ***
}
// order
public function sort($column)
{
$this->sortColumn = $column;
$this->sortDirection = $this->sortDirection == 'asc' ? 'desc' : 'asc';
}Explanation of the above code
The column of $columns we define the columns that we are going to show in the list, like; for example:
'id' => "id",As the key of the array, we indicate the name of the column in the database (the name of the property in the model).
As value, it is the label or text that we are going to place as header in the table.
The $sortColumn and $sortDirection properties define the column that we are going to sort at a given time and the type (ascending or descending), respectively.
In the view:
resources/views/livewire/dashboard/post/index.blade.php
***
<table class="table w-full border">
<thead class="text-left bg-gray-100 ">
<tr class="border-b">
@foreach ($columns as $key => $c)
<th>
<button wire:click="sort('{{ $key }}')">
{{ $c }}
@if ($key == $sortColumn)
@if ($sortDirection == 'asc')
↑
@else
↓
@endif
@endif
</button>
</th>
@endforeach
<th class="p-2">
Actions
</th>
</tr>
</thead>
***Filters on a list or table in Laravel Livewire
Filters in web development are used to classify or limit data in a list, they are widely used in administrative modules to limit the data in a list. For example, a filter can be used to display only those elements of a data set that meet with certain criteria, such as a specific date or a particular category; we can actually place any type of field; in practice, filters are nothing more than HTML forms with their respective HTML fields; in which requests are usually sent via GET and not POST, since the aim is to classify data and not change any type of data. Let's see how to use them in Livewire.
Filters in Laravel Livewire
In Laravel Livewire, we can easily apply filters to search our lists, thanks to the fact that there is a kind of synchronization or link between the client side and the server, as far as its properties are concerned, this type of operation is very easy to carry out.
These lists can be anything like a table or list structured based on DIVs.
In this article, we are going to start with a model of publications like the following:
class Post extends Model
{
use HasFactory;
protected $fillable=['title', 'slug','date','image','text','description','posted','type', 'category_id'];
protected $casts =[
'date' => 'datetime'
];
public function category()
{
return $this->belongsTo(Category::class);
}
}And the typical listing using a pagination:
<table class="table w-full border">
<thead class="text-left bg-gray-100 ">
<tr class="border-b">
@foreach ($columns as $key => $c)
<th>
<button wire:click="sort('{{ $key }}')">
{{ $c }}
@if ($key == $sortColumn)
@if ($sortDirection == 'asc')
↑
@else
↓
@endif
@endif
</button>
</th>
@endforeach
<th class="p-2">
Acciones
</th>
</tr>
</thead>
<tbody>
@foreach ($posts as $p)
<tr class="border-b">
<td class="p-2">
{{ $p->id }}
</td>
<td class="p-2">
{{ str($p->title)->substr(0, 15) }}
</td>
<td class="p-2">
{{ $p->date }}
</td>
<td class="p-2">
<textarea class="w-48">
{{ $p->description }}
</textarea>
</td>
<td class="p-2">
{{ $p->posted }}
</td>
<td class="p-2">
{{ $p->type }}
</td>
<td class="p-2">
{{ $p->category->title }}
</td>
<td class="p-2">
<x-jet-nav-link href="{{ route('d-post-edit', $p) }}" class="mr-2">Editar
</x-jet-nav-link>
<x-jet-danger-button {{-- onclick="confirm('Seguro que deseas eliminar el registro seleccionado?') || event.stopImmediatePropagation()" --}}
wire:click="seletedPostToDelete({{ $p }})">
Eliminar
</x-jet-danger-button>
</td>
</tr>
@endforeach
</tbody>
</table>
<br>
{{ $posts->links() }}To filter data in the component class, we use where conditions, in this case on the type:
if ($this->type) {
$posts->where('type', $this->type);
}The category:
if ($this->category_id) {
$posts->where('category_id', $this->category_id);
}And if it is posted or not:
if ($this->posted) {
$posts->where('posted', $this->posted);
}All are existing model structures and of course, you can customize.
So, our component to get the filtered posts:
The component class:
//***
// filters
public $type;
public $category_id;
public $posted;
//***
public function render()
{
$posts = Post::where("id", ">=", 1);
if ($this->type) {
$posts->where('type', $this->type);
}
if ($this->category_id) {
$posts->where('category_id', $this->category_id);
}
if ($this->posted) {
$posts->where('posted', $this->posted);
}
$categories = Category::pluck("title", "id");
$posts = $posts->paginate(10);
return view('livewire.dashboard.post.index', compact('posts', 'categories'));
}
//***Important to note that:
- We have one property per filter.
- The query to the database to obtain the posts is composed in several steps and it is verified if the filter properties have a value set to apply in the query.
In the view, we place our filters based on the structure we have:
<div class="flex gap-2 mb-2">
<select class="block w-full" wire:model="posted">
<option value="">{{__("Posted")}}</option>
<option value="not">No</option>
<option value="yes">Si</option>
</select>
<select class="block w-full" wire:model="type">
<option value="">{{__("Type")}}</option>
<option value="adverd">adverd</option>
<option value="post">post</option>
<option value="course">course</option>
<option value="movie">movie</option>
</select>
<select class="block w-full" wire:model="category_id">
<option value="">{{__("Category")}}</option>
@foreach ($categories as $i => $c)
<option value="{{ $i }}">{{ $c }}</option>
@endforeach
</select>
</div>
<table class="table w-full border">
//***At this point, we would have a paginated list, it is interesting to realize that we can use the properties in Livewire in conjunction with the wire:model not only for record management, but also for other purposes as in this case, it would be to apply a filter.
In the end, we will have:

And with this, we have a simple filter, remember that you can place more fields following the same organization as the one presented in this post; filters are one of the many topics covered in the Laravel Livewire course and book.
Paged listing with Volt
It is very easy to convert a classic Livewire component, that is, a class and view, to a Volt component. We will start with:
resources/views/livewire/dashboard/category/index.blade.php
<div>
<x-action-message on="deleted">
<div class="box-action-message">
{{ __("Category delete success") }}
</div>
</x-action-message>
<flux:heading>{{ __('Category List') }}</flux:heading>
<flux:text class="mt-2">Lorem ipsum dolor sit amet consectetur adipisicing.</flux:text>
<div class="separation"></div>
<flux:button class="ml-1 mb-3" variant='primary' icon="plus" href="{{ route('d-category-create') }}">
{{ __('Create') }}
</flux:button>
<!-- <flux:modal.trigger name="delete-category">
<flux:button>Delete</flux:button>
</flux:modal.trigger> -->
<flux:modal name="delete-category">
<div class="m-1">
<flux:heading>{{ __('Delete Category') }}</flux:heading>
<flux:text class="mt-2">{{ __('Are you sure you want to delete this category?') }}</flux:text>
<div class="flex flex-row-reverse">
<flux:button class="mt-4" variant='danger' icon="trash" wire:click="delete()">
{{ __('Delete') }}
</flux:button>
</div>
</div>
</flux:modal>
<div class="overflow-x-auto shadow-md rounded-lg">
<table class="table w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="rounded-lg text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th>
Id
</th>
<th>
Title
</th>
<th>
Actions
</th>
</tr>
</thead>
<tbody>
@foreach ($categories as $c)
<tr
class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700 border-gray-200">
<td>
{{ $c->id }}
</td>
<td>
{{ $c->title }}
</td>
<td>
<a href="{{ route('d-category-edit', $c) }}">Edit</a>
<flux:modal.trigger wire:click="selectCategodyToDelete({{ $c }})" name="delete-category">
<flux:button class="ml-3" variant='danger' size="xs">{{ __('Delete') }}</flux:button>
</flux:modal.trigger>
<!-- <flux:button class="ml-3" variant='danger' size="xs" wire:click="delete({{ $c }})">
{{ __('Delete') }}
</flux:button> -->
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<br>
{{ $categories->links() }}
</div>app/Livewire/Dashboard/Category/Index.php
<?php
namespace App\Livewire\Dashboard\Category;
use Livewire\Component;
use Livewire\WithPagination;
use App\Models\Category;
use Flux\Flux;
class Index extends Component
{
use WithPagination;
public $categoryToDelete;
public function render()
{
$categories = Category::paginate(10);
return view('livewire.dashboard.category.index', compact('categories'));
}
function selectCategodyToDelete(Category $category){
$this->categoryToDelete = $category;
}
function delete(){
$this->dispatch("deleted");
Flux::modal("delete-category")->close();
$this->categoryToDelete->delete();
}
}And a Volt component is exactly the same:
$ php artisan make:volt Volt/Dashboard/Category/Index<?php
use Livewire\Volt\Component;
use Illuminate\Support\Facades\Storage;
use Livewire\WithFileUploads;
use App\Models\Category;
new class extends Component {
use WithFileUploads;
public $title;
public $slug;
public $text;
public $image;
protected $rules = [
'title' => "required|min:2|max:255",
'text' => "nullable",
'image' => "nullable|image|max:1024",
];
public $category;
public function mount(?int $id = null)
{
if ($id != null) {
$this->category = Category::findOrFail($id);
$this->title = $this->category->title;
$this->text = $this->category->text;
}
}
function submit()
{
// validate
$this->validate();
if ($this->category) {
$this->category->update([
'title' => $this->title,
'text' => $this->text,
]);
$this->dispatch("updated");
} else {
$this->category = Category::create([
'title' => $this->title,
'slug' => str($this->title)->slug(),
'text' => $this->text,
]);
$this->dispatch("created");
}
// upload
if ($this->image) {
// delete old img
if ($this->category->image) {
Storage::disk('public_upload')
->delete('images/category/' . $this->category->image);
}
$imageName = $this->category->slug . '.' . $this->image->getClientOriginalExtension();
$this->image->storeAs('images/category', $imageName, 'public_upload');
$this->category->update([
'image' => $imageName
]);
}
}
}; ?>
<div>
<x-action-message on="created">
<div class="box-action-message">
{{ __('Created category success') }}
</div>
</x-action-message>
<x-action-message on="updated">
<div class="box-action-message">
{{ __('Updated category success') }}
</div>
</x-action-message>
<flux:heading>
@if ($category)
{{ __('Category edit: ') }} <span class="font-bold">{{ $category->title }}</span>
@else
{{ __('Category create') }}
@endif
</flux:heading>
<flux:text class="mt-2">Lorem ipsum dolor sit amet consectetur adipisicing.</flux:text>
<div class="separation"></div>
<form wire:submit.prevent="submit" class="flex flex-col gap-4">
<!-- <input type="text" wire:model="title">
<textarea wire:model="text"></textarea>
<button type="submit" wire:click="submit">Send</button> -->
{{-- <input type="text" wire:model="title"> --}}
{{-- @error('title')
{{ $message }}
@enderror --}}
<flux:input wire:model="title" :label="__('Title')" />
<flux:textarea wire:model="text" :label="__('Text')" />
<flux:input wire:model="image" type='file' :label="__('Image')" />
{{-- @error('text')
{{ $message }}
@enderror --}}
<div>
<flux:button variant="primary" type="submit">
{{ __('Save') }}
</flux:button>
</div>
</form>
@if ($category && $category->image)
<img class="w-40 my-3" src="{{ $category->getImageUrl() }}" />
@endif
</div>We create the route:
// demo volt
Volt::route('volt', 'volt.dashboard.category.index')->name('volt-d-category-index');As you can see, we removed the render method and the pagination case is special, we must use the with() method and return the data to the view.
The next step is to learn how to upload files or images using forms and drag and drop in Laravel Livewire.