Courses

Livewire 3 for Beginners with Laravel 12 Starter Kit

Category ToggleGroup and Table Filter

Summary of this lesson:
- Adding categories to task creation and editing forms.
- Syncing selected categories with task data on save.
- Displaying task categories in tasks table view.
- Implementing category filtering for task list display.

This lesson will tackle two things: choosing categories for the tasks in the form/table and filtering the tasks by category.


Create Task Form: Choose Category

First, we need to modify the Livewire component to use Categories. I will add the code for both Create and Edit forms right away.

app/Livewire/Tasks/Create.php

use App\Models\TaskCategory;
 
use App\Models\Task;
use Livewire\Component;
use Illuminate\View\View;
use Livewire\WithFileUploads;
use Livewire\Attributes\Validate;
 
class Create extends Component
{
// ...
 
public function render(): View
{
return view('livewire.tasks.create');
return view('livewire.tasks.create', [
'categories' => TaskCategory::all(),
]);
}
}

app/Livewire/Tasks/Edit.php:

use App\Models\TaskCategory;
 
class Edit extends Component
{
// ...
 
public function render(): View
{
return view('livewire.tasks.edit');
return view('livewire.tasks.edit', [
'categories' => TaskCategory::all(),
]);
}
}

Next, we can show categories in both create and edit pages. The styling is from a Flowbite checkbox component.

resources/views/livewire/tasks/create.blade.php:

<section class="max-w-5xl">
<form wire:submit="save" class="flex flex-col gap-6">
<flux:input
wire:model="name"
:label="__('Task Name')"
required
badge="required"
/>
 
<flux:input
wire:model="due_date"
type="date"
:label="__('Due Date')"
/>
 
<flux:input
wire:model="media"
type="file"
:label="__('Media')"
/>
 
<flux:label>
Categories
</flux:label>
 
<ul class="items-center w-full text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg sm:flex dark:bg-gray-700 dark:border-gray-600 dark:text-white">
@foreach($categories as $category)
<li class="w-full border-b border-gray-200 sm:border-b-0 sm:border-r dark:border-gray-600">
<div class="flex items-center ps-3">
<input wire:model="selectedCategories" id="{{ $category->id }}-checkbox-list" type="checkbox" value="{{ $category->id }}" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500">
<label for="{{ $category->id }}-checkbox-list" class="w-full py-3 ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">{{ $category->name }}</label>
</div>
</li>
@endforeach
</ul>
 
<div>
<flux:button variant="primary" type="submit">{{ __('Save') }}</flux:button>
</div>
</form>
</section>

resources/views/livewire/tasks/edit.blade.php:

<section class="max-w-5xl">
<form wire:submit="save" class="flex flex-col gap-6">
<flux:input
wire:model="name"
:label="__('Task Name')"
required
badge="required"
/>
 
<flux:switch
wire:model="is_completed"
label="Completed?"
align="left"
/>
 
<flux:input
wire:model="due_date"
type="date"
:label="__('Due Date')"
/>
 
<flux:input
wire:model="media"
type="file"
:label="__('Media')"
/>
 
@if($task->media_file)
<a href="{{ $task->media_file->original_url }}" target="_blank">
<img src="{{ $task->media_file->original_url }}" alt="{{ $task->name }}" class="w-32 h-32" />
</a>
@endif
 
<flux:label>
Categories
</flux:label>
 
<ul class="items-center w-full text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg sm:flex dark:bg-gray-700 dark:border-gray-600 dark:text-white">
@foreach($categories as $category)
<li class="w-full border-b border-gray-200 sm:border-b-0 sm:border-r dark:border-gray-600">
<div class="flex items-center ps-3">
<input wire:model="selectedCategories" id="{{ $category->id }}-checkbox-list" type="checkbox" value="{{ $category->id }}" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500">
<label for="{{ $category->id }}-checkbox-list" class="w-full py-3 ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">{{ $category->name }}</label>
</div>
</li>
@endforeach
</ul>
 
<div>
<flux:button variant="primary" type="submit">{{ __('Save') }}</flux:button>
</div>
</form>
</section>

Behind the scenes, I've added three categories to the database:

With those, here's the visual result for the Create Task form:


Saving Data to the DB

We need to modify create and edit Livewire components to add public property where selected categories IDs will be saved. When saving simply sync categories to the task.

app/Livewire/Tasks/Create.php

class Create extends Component
{
use WithFileUploads;
 
#[Validate('required|string|max:255')]
public string $name = '';
 
#[Validate('nullable|date')]
public null|string $due_date = null;
 
#[Validate('nullable|file|max:10240')]
public $media;
 
#[Validate([
'selectedCategories' => ['nullable', 'array'],
'selectedCategories.*' => ['exists:task_categories,id'],
])]
public array $selectedCategories = [];
 
public function save(): void
{
$this->validate();
 
$task = Task::create([
'name' => $this->name,
'due_date' => $this->due_date,
]);
 
if ($this->media) {
$task->addMedia($this->media)->toMediaCollection();
}
 
$task->taskCategories()->sync($this->selectedCategories);
 
session()->flash('success', 'Task successfully created.');
 
$this->redirectRoute('tasks.index', navigate: true);
}
 
public function render(): View
{
return view('livewire.tasks.create', [
'categories' => TaskCategory::all(),
]);
}
}

For the edit Livewire component we also need to assign selected categories from the database to the public property.

class Edit extends Component
{
use WithFileUploads;
 
#[Validate('required|string|max:255')]
public string $name;
 
#[Validate('nullable|boolean')]
public bool $is_completed;
 
#[Validate('nullable|date')]
public null|string $due_date = null;
 
#[Validate('nullable|file|max:10240')]
public $media;
 
#[Validate([
'selectedCategories' => ['nullable', 'array'],
'selectedCategories.*' => ['exists:task_categories,id'],
])]
public array $selectedCategories = [];
 
public Task $task;
 
public function mount(Task $task): void
{
$this->task = $task;
$this->task->load('media');
$this->task->load('media', 'taskCategories');
$this->name = $task->name;
$this->is_completed = $task->is_completed;
$this->due_date = $task->due_date?->format('Y-m-d');
$this->selectedCategories = $task->taskCategories->pluck('id')->toArray();
}
 
public function save(): void
{
$this->validate();
 
$this->task->update([
'name' => $this->name,
'is_completed' => $this->is_completed,
'due_date' => $this->due_date,
]);
 
if ($this->media) {
$this->task->getFirstMedia()?->delete();
$this->task->addMedia($this->media)->toMediaCollection();
}
 
$this->task->taskCategories()->sync($this->selectedCategories, []);
 
session()->flash('success', 'Task successfully updated.');
 
$this->redirectRoute('tasks.index', navigate: true);
}
 
public function render(): View
{
return view('livewire.tasks.edit', [
'categories' => TaskCategory::all(),
]);
}
}

Show Categories in the Tasks List Table

First, we need to eager load the categories in the Tasks/Index Livewire component.

app/Livewire/Tasks/Index.php:

class Index extends Component
{
// ...
 
public function render(): View
{
return view('livewire.tasks.index', [
'tasks' => Task::with('media')->paginate(3),
'tasks' => Task::with('media', 'taskCategories')->paginate(3),
]);
}
}

And then, let's add the categories to the Livewire page tasks list.

resources/views/livewire/tasks/index.blade.php:

// ...
 
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">
Task
</th>
<th scope="col" class="px-6 py-3">
File
</th>
<th scope="col" class="px-6 py-3">
Status
</th>
<th scope="col" class="px-6 py-3">
Categories
</th>
<th scope="col" class="px-6 py-3">
Due date
</th>
<th scope="col" class="px-6 py-3">
Actions
</th>
</tr>
</thead>
<tbody>
@foreach($tasks as $task)
<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">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{ $task->name }}
</th>
<td class="px-6 py-4">
@if($task->media_file)
<a href="{{ $task->media_file->original_url }}" target="_blank">
<img src="{{ $task->media_file->original_url }}" alt="{{ $task->name }}" class="w-8 h-8" />
</a>
@endif
</td>
<td class="px-6 py-4">
<span @class([
'text-green-600' => $task->is_completed,
'text-red-700' => ! $task->is_completed,
])>
{{ $task->is_completed ? 'Completed' : 'In progress' }}
</span>
</td>
<td class="px-6 py-4 space-x-1">
@foreach($task->taskCategories as $category)
<span class="rounded-full bg-gray-200 px-2 py-1 text-xs">
{{ $category->name }}
</span>
@endforeach
</td>
<td class="px-6 py-4">
{{ $task->due_date?->format('M d, Y') }}
</td>
<td class="px-6 py-4 space-x-2">
<flux:button href="{{ route('tasks.edit', $task) }}" variant="filled">{{ __('Edit') }}</flux:button>
<flux:button wire:confirm="Are you sure?" wire:click="delete({{ $task->id }})" variant="danger" type="button">{{ __('Delete') }}</flux:button>
</td>
</tr>
@endforeach
</tbody>
</table>
 
// ...

Here's the visual result:

Great, now our Tasks CRUD contains the Category selection!


Filter in Tasks Table

The final thing in this lesson is to show the Tasks List with the ability to filter by categories.

First, we must pass categories to the front end, and then we can show them.

app/Livewire/Tasks/Index.php:

use App\Models\TaskCategory;
 
class Index extends Component
{
// ...
 
public function render(): View
{
return view('livewire.tasks.index', [
'tasks' => Task::with('media', 'taskCategories')->paginate(3),
'categories' => TaskCategory::all(),
]);
}
}

resources/views/livewire/tasks/index.blade.php:

<section>
<x-alerts.success />
 
<div class="flex flex-grow gap-x-4 mb-4">
<flux:button href="{{ route('tasks.create') }}" variant="filled">{{ __('Create Task') }}</flux:button>
<flux:button href="{{ route('task-categories.index') }}" variant="filled">{{ __('Manage Task Categories') }}</flux:button>
</div>
 
<div class="mb-4 flex flex-row justify-end gap-x-2 w-full">
<ul class="items-center w-fit text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg sm:flex dark:bg-gray-700 dark:border-gray-600 dark:text-white">
@foreach($categories as $category)
<li class="w-full pr-2 border-b border-gray-200 sm:border-b-0 sm:border-r dark:border-gray-600">
<div class="flex items-center ps-3">
<input wire:model.live="selectedCategories" id="{{ $category->id }}-checkbox-list" type="checkbox" value="{{ $category->id }}" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500">
<label for="{{ $category->id }}-checkbox-list" class="w-full py-3 ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">{{ $category->name }}</label>
</div>
</li>
@endforeach
</ul>
</div>
 
// ...

It's very important that wire:model would have .live added so that filter would work without page refresh.

Now, you can see the filter above the table!

To filter by a category we need to add a public property where selected categories IDs will be saved and modify query where we get all the tasks.

app/Livewire/Tasks/Index.php:

use Livewire\Attributes\Url;
use Illuminate\Database\Eloquent\Builder;
 
class Index extends Component
{
use WithPagination;
 
#[Url(as: 'categories', except: '')]
public ?array $selectedCategories = [];
 
public function delete(int $id): void
{
Task::where('id', $id)->delete();
 
session()->flash('success', 'Task successfully deleted.');
}
 
public function render(): View
{
return view('livewire.tasks.index', [
'tasks' => Task::with('media', 'taskCategories')->paginate(3),
'tasks' => Task::query()
->with('media', 'taskCategories')
->when($this->selectedCategories, function (Builder $query) {
$query->whereHas('taskCategories', function (Builder $query) {
$query->whereIn('id', $this->selectedCategories);
});
})
->paginate(3),
'categories' => TaskCategory::all(),
]);
}
}

If you click any category, the tasks list will be filtered only for that category:

And also, you can filter by multiple categories!


The repository for this starter kit project section is here on GitHub.

avatar

Of these three Laravel 12 classes you have created this is the First one I have been able to complete and every thing works Good Job on this one.

P.S. I added the breadcrumbs to this.

avatar
You can use Markdown
avatar
You can use Markdown