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.
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.