Courses

Livewire 3 for Beginners with Laravel 12 Starter Kit

Second CRUD: Task Categories

Summary of this lesson:
- Setting up TaskCategory model, migration, and relationships
- Creating Livewire components for category management
- Displaying categories in a table with task count
- Adding buttons for category management in the tasks list

Let's practice creating a CRUD with another one: Task Categories. It will be almost the same as Tasks CRUD, so for the most part, I will just show the code and specify the differences.


Task Categories: DB Model/Migration

First, we prepare the back end.

Create Model, Migration, and Pivot table for task categories:

php artisan make:model TaskCategory -m
php artisan make:migration create_task_task_category_table

Migration:

Schema::create('task_categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});

Pivot migration:

Schema::create('task_task_category', function (Blueprint $table) {
$table->foreignId('task_id')->constrained();
$table->foreignId('task_category_id')->constrained();
});

app/Models/Task.php:

use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
// ...
 
public function taskCategories(): BelongsToMany
{
return $this->belongsToMany(TaskCategory::class);
}

app/Models/TaskCategory.php

namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
class TaskCategory extends Model
{
protected $fillable = ['name'];
 
public function tasks(): BelongsToMany
{
return $this->belongsToMany(Task::class);
}
}

Livewire Components and Routes

Create Livewire components:

php artisan make:livewire TaskCategories/Index
php artisan make:livewire TaskCategories/Create
php artisan make:livewire TaskCategories/Edit

app/Livewire/TaskCategories/Index.php:

use Livewire\Component;
use Illuminate\View\View;
use App\Models\TaskCategory;
 
class Index extends Component
{
public function delete(int $id): void
{
$taskCategory = TaskCategory::findOrFail($id);
 
if ($taskCategory->tasks()->count() > 0) {
$taskCategory->tasks()->detach();
}
 
$taskCategory->delete();
}
 
public function render(): View
{
return view('livewire.task-categories.index', [
'taskCategories' => TaskCategory::withCount('tasks')->paginate(5),
]);
}
}

app/Livewire/TaskCategories/Create.php:

use Livewire\Component;
use Illuminate\View\View;
use App\Models\TaskCategory;
use Livewire\Attributes\Validate;
 
class Create extends Component
{
#[Validate('required|string|max:255')]
public string $name = '';
 
public function save(): void
{
$this->validate();
 
TaskCategory::create([
'name' => $this->name,
]);
 
session()->flash('success', 'Task category successfully created.');
 
$this->redirectRoute('task-categories.index', navigate: true);
}
 
public function render(): View
{
return view('livewire.task-categories.create');
}
}

app/Livewire/TaskCategories/Edit.php:

use Livewire\Component;
use Illuminate\View\View;
use App\Models\TaskCategory;
use Livewire\Attributes\Validate;
 
class Edit extends Component
{
#[Validate('required|string|max:255')]
public string $name;
 
public TaskCategory $taskCategory;
 
public function mount(TaskCategory $taskCategory): void
{
$this->taskCategory = $taskCategory;
$this->name = $taskCategory->name;
}
 
public function save(): void
{
$this->validate();
 
$this->taskCategory->update([
'name' => $this->name,
]);
 
session()->flash('success', 'Task category successfully updated.');
 
$this->redirectRoute('task-categories.index', navigate: true);
}
 
public function render(): View
{
return view('livewire.task-categories.edit');
}
}

And assign them routes.

routes/web.php

use App\Livewire\TaskCategories;
 
// ...
 
Route::middleware(['auth'])->group(function () {
// ...
 
Route::get('task-categories', TaskCategories\Index::class)->name('task-categories.index');
Route::get('task-categories/create', TaskCategories\Create::class)->name('task-categories.create');
Route::get('task-categories/{taskCategory}/edit', TaskCategories\Edit::class)->name('task-categories.edit');
});
 
require __DIR__.'/auth.php';

Showing Categories Table

Here's what our table will look like. Again, it is very similar to the Task list.

resources/views/livewire/task-categories/index.blade.php:

<section>
<x-alerts.success />
 
<flux:button href="{{ route('task-categories.create') }}" variant="filled" class="mb-4">{{ __('Create Category') }}</flux:button>
 
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<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">
Name
</th>
<th scope="col" class="px-6 py-3">
Assigned Tasks
</th>
<th scope="col" class="px-6 py-3">
Actions
</th>
</tr>
</thead>
<tbody>
@foreach($taskCategories as $category)
<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">
{{ $category->name }}
</th>
<td class="px-6 py-4">
{{ $category->tasks_count }}
</td>
<td class="px-6 py-4 space-x-2">
<flux:button href="{{ route('task-categories.edit', $category) }}" variant="filled">{{ __('Edit') }}</flux:button>
<flux:button wire:confirm="Are you sure you want to delete category with {{ $category->tasks_count }} assigned tasks?" wire:click="delete({{ $category->id }})" variant="danger" type="button">{{ __('Delete') }}</flux:button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
 
@if($taskCategories->hasPages())
<div class="mt-5">
{{ $taskCategories->links() }}
</div>
@endif
</section>

Okay, now how do we get to that route to test it? We need a link or button, right?


Adding a Button to Manage Categories

In this case, I won't add a link in the main navigation. Rather, I will add a button at the top of the Tasks List, near the "Create Task" button.

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

BEFORE:

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

AFTER:

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

The visual result:

If we click that button, we see the empty (but working!) table:


Create Category Form

I'm not even sure if I need to comment anything here. It has a totally identical structure to the Create Task form from the previous lesson.

resources/views/livewire/task-categories/create.blade.php:

<section class="max-w-5xl">
<form wire:submit="save" class="flex flex-col gap-6">
<flux:input
wire:model="name"
:label="__('Name')"
required
badge="required"
/>
 
<div>
<flux:button variant="primary" type="submit">{{ __('Save') }}</flux:button>
</div>
</form>
</section>

Here's the visual result:


Edit Category Form

Again, very similar to the Task Edit form. Not much more to say.

resources/views/livewire/task-categories/edit.blade.php:

<section class="max-w-5xl">
<form wire:submit="save" class="flex flex-col gap-6">
<flux:input
wire:model="name"
:label="__('Name')"
required
badge="required"
/>
 
<div>
<flux:button variant="primary" type="submit">{{ __('Save') }}</flux:button>
</div>
</form>
</section>

Here's the visual result if we click Edit:

Now, we don't have category selection in the Task Create/Edit forms. We will add that in the next lesson.


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

Previous: File Upload with Spatie Media Library

No comments yet…

avatar
You can use Markdown