Appearance
Admin: Managing Genres
A fundamental part of any administrative backend is the ability to manage the core data of the application. For our vinyl shop, this includes managing the list of music genres. Administrators need the capability to add new genres, view existing ones, modify them, and remove them if necessary. These core operations are commonly known as CRUD (C reate, Read, Update, Delete). This chapter guides you through implementing full CRUD functionality for the genres
table using the power and simplicity of Livewire.
REMARK
Remember that you can always reset the database to its initial state by running the command:
bash
php artisan migrate:fresh --seed
1
DO THIS FIRST
Before starting this chapter, make sure you have created the Notification Components
Preparation
Before diving into the CRUD logic, we need to set up the basic structure for our genre management page. This involves creating the Livewire component, defining a route to access it, linking it from the admin navigation, and scaffolding the initial view.
Create the Genres Component
We'll encapsulate the genre management logic and presentation within a dedicated Livewire component. To keep our admin features organized, we'll place this component within an Admin
subdirectory.
- Open your terminal in the project directory.
- Execute the following Artisan command to generate a new Livewire component named
Admin/Genres
:
bash
php artisan livewire:make Admin/Genres
1
This command conveniently creates two essential files for us:
app/Livewire/Admin/Genres.php
: The component's PHP class, acting as the controller for our admin page. It starts with arender()
method responsible for returning the view.resources/views/livewire/admin/genres.blade.php
: The component's Blade view file, where we'll define the HTML structure for displaying and interacting with genres.
Add a New Route
To make our new admin page accessible via a URL, we need to define a route in Laravel's routing system. This route will map a specific URL path within the /admin
group to our Admin\Genres
Livewire component.
- Open the
routes/web.php
file. - Inside the
Route::middleware(['auth', ActiveUser::class, Admin::class])->prefix('admin')->group(...)
block, add a new route definition that maps the/admin/genres
URL path to theApp\Livewire\Admin\Genres
component class. - Assign the route a name,
admin.genres
, for easy URL generation using theroute()
helper function.
php
Route::middleware(['auth', ActiveUser::class, Admin::class])->prefix('admin')->group(function () {
Route::redirect('/', '/admin/records');
Route::get('records', Demo::class)->name('admin.records');
Route::get('genres', Genres::class)->name('admin.genres');
Route::view('download_covers', 'admin.download_covers')->name('download_covers');
});
1
2
3
4
5
6
2
3
4
5
6
Now, let's link this new route from the admin section of our main navigation bar.
- Open the
resources/views/components/layouts/vinylshop/navbar.blade.php
file. - Locate the
<flux:navlist.item>
for the "Genres" link within the@auth
block. - Update the
href
attribute to dynamically generate the URL using the route name:{{ route('admin.genres') }}
.
php
@auth
<flux:separator variant="subtle"/>
<flux:navlist.group expandable heading="Admin">
<flux:navlist.item href="{{ route('admin.genres') }}">Genres</flux:navlist.item>
{{-- Other admin links --}}
</flux:navlist.group>
@endauth
1
2
3
4
5
6
7
2
3
4
5
6
7
Basic Scaffolding for the View
With the component and route ready, let's set up the initial structure for the genres.blade.php
view. We'll define the page title and description using named slots, add a placeholder for creating new genres, and set up the basic table structure where genres will be listed.
- Open
resources/views/livewire/admin/genres.blade.php
. - Replace its content with the following structure:
- Add
<x-slot>
directives for thetitle
anddescription
. - Include a
<flux:input>
field (we'll wire this up later for creating a new genre). - Use the custom
<x-itf.table>
component to define the table structure with sortable headers for ID, Genre, and Record Count. - Add a placeholder table row (
<tr>
) to visualize the structure. - Include the
<x-itf.livewire-log/>
component for debugging.
- Add
php
<div>
<x-slot:title>Genres</x-slot:title>
<x-slot:description>Manage music genres</x-slot:description>
<div class="flex items-start gap-4 mb-4">
<flux:input
class="w-56"
icon="plus" placeholder="Create new genre" label="" clearable/>
</div>
<x-itf.table cols="w-12, w-52, w-24, w-auto">
<thead>
<tr>
<x-itf.table.sortable-header>ID</x-itf.table.sortable-header>
<x-itf.table.sortable-header>Genre</x-itf.table.sortable-header>
<x-itf.table.sortable-header>Records</x-itf.table.sortable-header>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>
<flux:button
icon="pencil-square"/>
<flux:button
icon="trash"/>
</td>
</tr>
</tbody>
</x-itf.table>
<x-itf.livewire-log/>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Read All Genres (The "R" in CRUD)
The first step in managing genres is displaying the existing ones. We'll modify our Livewire component to fetch genre data from the database and pass it to the view, which will then render it in the table. We'll also implement sorting functionality.
Show All Genres in the Table
We'll query theGenre
model in the render()
method, eager-load the count of associated records using withCount()
, apply default sorting, and pass the results to the Blade view. The view will then loop through this data to build the table rows.
app/Livewire/Admin/Genres.php:
- Import the
App\Models\Genre
class.
- Add public properties
$sortColumn
(default 'name') and$sortDirection
(default 'asc') to control the table sorting. - In the
render()
method:- Query the
Genre
model. - Use
withCount('records')
to efficiently get the number of records associated with each genre. This avoids the N+1 query problem. - Apply sorting using
orderBy($this->sortColumn, $this->sortDirection)
. - Fetch the results using
->get()
. (We'll add pagination later). - Pass the fetched
$genres
collection to the view usingcompact('genres')
.
- Query the
- Import the
resources/views/livewire/admin/genres.blade.php:
- Inside the
<tbody>
tag, replace the placeholder<tr>
with a Blade@foreach($genres as $genre)
loop. - Add the
wire:key="{{ $genre->id }}"
directive to the<tr>
element inside the loop. This is crucial for Livewire's DOM diffing and updating mechanism, ensuring efficient updates when the data changes. - Populate the table cells (
<td>
) with data from the$genre
object:$genre->id
,$genre->name
, and$genre->records_count
(the count provided bywithCount
). - Pass the
$genres
collection to the debugging component:<x-itf.livewire-log :genres="$genres" />
.
- Inside the
php
class Genres extends Component
{
// sort properties
public $sortColumn = 'name';
public $sortDirection = 'asc';
public function render()
{
$genres = Genre::withCount('records')
->orderBy($this->sortColumn, $this->sortDirection)
->get();
return view('livewire.admin.genres', compact('genres'));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Order the Table by Clicking Column Headers
To make the table interactive, we'll allow users to sort the data by clicking on the column headers. This involves adding a method to the Livewire component to handle the sorting logic and updating the view to trigger this method and reflect the current sort state.
- app/Livewire/Admin/Genres.php:
- Add a public method
resort($column)
. - Inside
resort()
:- Check if the clicked
$column
is the same as the current$sortColumn
. - If yes, toggle the
$sortDirection
('asc' becomes 'desc', 'desc' becomes 'asc'). - If no, set
$sortColumn
to the new$column
and reset$sortDirection
to 'asc'. - Livewire will automatically re-render the component after this public method executes, applying the new sorting in the
render()
method's query.
- Check if the clicked
- Add a public method
- resources/views/livewire/admin/genres.blade.php:
- For each
<x-itf.table.sortable-header>
component in the<thead>
:- Add a
wire:click
directive to call theresort()
method, passing the corresponding database column name ( e.g.,wire:click="resort('id')"
). - Bind the
:sorted
attribute to a boolean expression checking if the header's column matches the component's$sortColumn
(e.g.,:sorted="$sortColumn === 'id'"
). - Bind the
:direction
attribute directly to the component's$sortDirection
.
- Add a
- For each
php
class Genres extends Component
{
// sort properties
public $sortColumn = 'name';
public $sortDirection = 'asc';
// Method to handle column sorting
public function resort($column)
{
// If clicking the same column, toggle direction
if ($this->sortColumn === $column) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
}
// Otherwise, set new column and default direction
else {
$this->sortColumn = $column;
$this->sortDirection = 'asc';
}
}
public function render() { /* ... */}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Create a New Genre (The "C" in CRUD)
Now let's implement the ability to add new genres to the database directly from the admin interface. We'll use the input field we added earlier, validate the input, and save the new genre.
Add a New Genre
We need a public property to bind to the input field and a method to handle the creation logic. We'll also add validation to ensure the genre name isn't empty, meets length requirements, and is unique.
- app/Livewire/Admin/Genres.php:
- Add a public property
$newGenreName = '';
. - Use Livewire 3's
#[Validate]
attribute above the property to define validation rules directly. We'll make the namerequired
, check its length (min:3|max:30
), and ensure the name isunique
in thegenres
table (unique:genres,name
). - Add a public method
createNewGenre()
. - Inside
createNewGenre()
:- Call
$this->validateOnly('newGenreName')
to validate only this specific property based on its#[Validate]
attribute. - If validation passes, create a new
Genre
record usingGenre::create(['name' => trim($this->newGenreName)])
. (Thetrim()
function removes any leading or trailing whitespace from the input before saving it. This ensures clean data in the database and prevents issues like having ' Minimal ' and 'Minimal ' as separate genres.) - Reset the input field by calling
$this->reset('newGenreName');
. Livewire automatically re-renders.
- Call
- Add a public property
- resources/views/livewire/admin/genres.blade.php:
- Update the
<flux:input>
field:- Add
wire:model.live.debounce.500ms
to bind it to the propertynewGenreName
. The.live
modifier updates the property on the backend as the user types (debounced by 500ms), allowing for real-time validation feedback if desired. - Add
wire:keydown.enter="createNewGenre()"
to trigger the creation method when the user presses Enter in the input field.
- Add
- Update the
php
<?php
namespace App\Livewire\Admin;
use App\Models\Genre;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Genres extends Component
{
// sort properties
public $sortColumn = 'name';
public $sortDirection = 'asc';
// Property for the new genre input field
#[Validate('required|min:3|max:30|unique:genres,name')] // Add validation rules
public $newGenreName = '';
public function resort($column) {/* ... */ }
// Method to create a new genre
public function createNewGenre()
{
// Validate only the newGenreName property based on its #[Validate] attribute
$this->validateOnly('newGenreName');
// Create the new genre
Genre::create([
'name' => trim($this->newGenreName)
]);
// Reset the input field
$this->reset('newGenreName');
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Update the Validation Message
When Livewire performs validation using the #[Validate]
attribute, and a rule fails (like required
or min:3
), it generates an error message for the user. By default, Livewire tries to create a user-friendly name for the input field based on the property name in your component class.
- It takes the property name (e.g.,
$newGenreName
). - It converts it from camelCase (or snake_case) into a more readable format, usually lowercase words separated by spaces (
new genre name
). - It inserts this generated name into the validation message template (e.g.,
The new genre name field is required.
orThe new genre name must be at least 3 characters.
).
While this automatic conversion is often helpful, sometimes the result isn't ideal:
- Awkward Phrasing:
new genre name
might sound a bit clunky. You might prefer just genre name or name for this genre. - Lack of Context: The property name might be clear to you as a developer, but less clear to the end-user in the context of the error message.
The Solution: Customizing the Attribute Name with as:
Livewire provides a way to explicitly tell the validation system what "name" to use for a specific property when generating error messages. You do this by adding the as:
parameter inside the #[Validate]
attribute definition. Let's make the message refer to "name for this genre" instead of the default "new genre name".
- app/Livewire/Admin/Genres.php:
- Modify the
#[Validate]
attribute for$newGenreName
by adding, as: 'name for this genre'
.
- Modify the
php
// Property for the new genre input field
#[Validate('required|min:3|max:30|unique:genres,name', as: 'name for this genre' )]
public $newGenreName = '';
1
2
3
2
3
Add a Toast Response
Providing feedback after an action is crucial for a good user experience. When a new genre is successfully created, we should inform the user with a clear message. We'll use the NotificationsTrait
(which you should have set up as per the "DO THIS FIRST" tip) to display a "toast" notification.
- app/Livewire/Admin/Genres.php:
- Import the
App\Traits\NotificationsTrait
at the top of the class file. - Add
use NotificationsTrait;
inside the class definition. - At the end of the
createNewGenre()
method, after the genre is created and the input is reset, call$this->toastSuccess()
. - Provide a descriptive message, including the name of the newly created genre. You can even use HTML within the toast message.
- Optionally, configure the toast's appearance (like duration or position) using the second argument of the toast method.
- Import the
php
<?php
namespace App\Livewire\Admin;
use App\Models\Genre;
use App\Traits\NotificationsTrait;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Genres extends Component
{
use NotificationsTrait;
// sort properties
public $sortColumn = 'name';
public $sortDirection = 'asc';
// Property for the new genre input field
#[Validate('required|min:3|max:30|unique:genres,name', as: 'name for this genre' )] // Add validation rules
public $newGenreName = '';
// Method to handle column sorting
public function resort($column) { /* ... */ }
// Method to create a new genre
public function createNewGenre()
{
// Validate only the newGenreName property based on its #[Validate] attribute
$this->validateOnly('newGenreName');
// Create the new genre
$genre = Genre::create([
'name' => trim($this->newGenreName)
]);
// Reset the input field
$this->reset('newGenreName');
// Add a toast response
$this->toastSuccess(
"The genre <b><i>$genre->name</i></b> has been added",
[
'duration' => 0,
'position' => 'top-right'
]
);
}
public function render() { /* ... */ }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Clear the Input Field and Validation on Escape/Blur
Currently, the input field is only cleared after successfully creating a genre. If the user starts typing, gets a validation error, and then wants to cancel, the error message remain. The <flux:input>
component's clearable
prop adds an 'x' button, but clicking it does not reset Livewire's validation state.
Let's add functionality to clear both the input value and any associated validation errors when the user presses the Escape key or clicks outside the input field (the blur
event).
- app/Livewire/Admin/Genres.php:
- Add a new public method
resetValues()
. - Inside
resetValues()
, call$this->reset('newGenreName')
to reset the property to its initial empty state. - Also call
$this->resetErrorBag()
to clear any validation messages currently displayed for any field in the component.
- Add a new public method
- resources/views/livewire/admin/genres.blade.php:
- Add
wire:keydown.esc="resetValues()"
to the<flux:input>
to trigger the method when Escape is pressed. - Add
wire:blur="resetValues()"
to trigger the method when the input field loses focus.
- Add
php
class Genres extends Component
{
use NotificationsTrait;
// sort properties
public $sortColumn = 'name';
public $sortDirection = 'asc';
// Property for the new genre input field
#[Validate('required|min:3|max:30|unique:genres,name', as: 'name for this genre' )] // Add validation rules
public $newGenreName = '';
// Method to handle column sorting
public function resort($column) { /* ... */ }
public function resetValues()
{
$this->reset('newGenreName');
$this->resetErrorBag();
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Update a Genre (The "U" in CRUD)
Administrators also need to be able to correct typos or rename existing genres. We'll implement an inline editing feature: clicking an "Edit" button (pencil icon) next to a genre will turn its name into an input field, allowing direct modification.
Preparing for Edit Mode
To enable editing, we need component properties to track which specific genre is currently being edited and to hold its name while the input field is active. Clicking the "Edit" button will populate these properties and trigger the display of the input field.
- app/Livewire/Admin/Genres.php:
- Add two new public properties:
$editingGenreId = null;
: This will store the databaseid
of the genre the user wants to edit. It'snull
when no genre is being edited.$editingGenreName = '';
: This will be bound to the input field displayed during editing.
- Apply the same validation rules to
$editingGenreName
as we did for$newGenreName
using the#[Validate]
attribute. This ensures consistency and prevents saving invalid or duplicate names during an update. - Add a public method
editGenre(Genre $genre)
. We use Route Model Binding here by type-hintingGenre $genre
. Livewire will automatically fetch the correctGenre
model based on the ID passed from the view. - Inside
editGenre()
:- Set
$this->editingGenreId = $genre->id;
. This marks the specific genre row for editing. - Set
$this->editingGenreName = $genre->name;
. This pre-fills the edit input field with the genre's current name.
- Set
- Modify the
resetValues()
method to also reset$editingGenreId
and$editingGenreName
back to their initial states (null
and''
respectively). This ensures that canceling an edit (via Esc or blur) properly clears the editing state.
- Add two new public properties:
- resources/views/livewire/admin/genres.blade.php:
- Locate the "Edit" button (
<flux:button icon="pencil-square">
) inside the@foreach($genres as $genre)
loop. - Add a
wire:click
directive to call theeditGenre
method, passing the current genre's model ($genre
):wire:click="editGenre({{ $genre->id }})"
.
Note: Livewire implicitly passes the ID here, which works with the Route Model Binding in the component method. - Add a tooltip
tooltip="Edit {{ $genre->name }}"
to the button.
- Locate the "Edit" button (
php
<?php
namespace App\Livewire\Admin;
use App\Models\Genre;
use App\Traits\NotificationsTrait;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Genres extends Component
{
use NotificationsTrait;
// sort properties
public $sortColumn = 'name';
public $sortDirection = 'asc';
// Property for the new genre input field
#[Validate('required|min:3|max:30|unique:genres,name', as: 'name for this genre')]
public $newGenreName = '';
// Properties for editing a genre
public ?int $editingGenreId = null; // Use ?int for type safety, allowing null
#[Validate('required|min:3|max:30|unique:genres,name', as: 'name for this genre')]
public string $editingGenreName = '';
// Method to handle column sorting
public function resort($column) { /* ... */ }
// Method to clear input and validation states
public function resetValues()
{
// Reset create *and* edit properties
$this->reset('newGenreName', 'editingGenreId', 'editingGenreName');
$this->resetErrorBag();
}
// Method to set up edit mode for a specific genre
public function editGenre(Genre $genre)
{
// Set the ID of the genre being edited
$this->editingGenreId = $genre->id;
// Pre-fill the input with the current name
$this->editingGenreName = $genre->name;
}
// Method to create a new genre
public function createNewGenre() { /* ... */ }
// Add render method
public function render() { /* ... */ }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Implementing Inline Edit and Update Logic
Now we'll modify the view to conditionally display either the genre name or an input field based on the $editingGenreId
. We'll also implement the updateGenre
method to handle saving the changes.
- resources/views/livewire/admin/genres.blade.php:
- Inside the
<td>
for the genre name within the@foreach
loop:- Add an
@if($editingGenreId === $genre->id)
check. - If
true
(this row is being edited):- Display a
<flux:input>
field. - Bind it to the
$editingGenreName
property usingwire:model="editingGenreName"
. - Add
x-init="$el.focus()"
to set the cursor on the input field.$el
refers to the element itself (the input field) andfocus()
is a regular JavaScript function to sets the focus on the input field. - Add
wire:keydown.enter="updateGenre({{ $genre->id }})"
to save changes when Enter is pressed. - Add
wire:keydown.escape="resetValues()"
to cancel editing when Escape is pressed. - Add
wire:blur="resetValues()"
to cancel editing when the input loses focus (user clicks away).
- Display a
- If
false
(this row is not being edited):- Simply display the genre name:
{{ $genre->name }}
.
- Simply display the genre name:
- Add an
- Inside the
- app/Livewire/Admin/Genres.php:
- Add a public method
updateGenre(Genre $genre)
(again using Route Model Binding). - Inside
updateGenre()
:- Trim Input:
trim()
the$this->editingGenreName
to remove leading/trailing whitespace. - Check for Changes: Compare the trimmed, lowercase input (
strtolower($this->editingGenreName)
) with the original, lowercase name (strtolower($genre->name)
). Also check if the input is empty. If the name hasn't actually changed or is empty, call$this->resetValues()
to exit edit mode without doing anything else, andreturn
. This prevents unnecessary database queries and validation errors if the user didn't modify the name or deleted it. - Validate: Call
$this->validateOnly('editingGenreName')
to ensure the new name meets the validation requirements (required, length, unique). - Store Old Name: Keep the original
$genre->name
in a temporary variable ($oldName
) to use in the success message. - Update Database: Call
$genre->update(['name' => $this->editingGenreName])
to save the trimmed, validated name. - Reset State: Call
$this->resetValues()
to clear the editing properties ($editingGenreId
,$editingGenreName
) and any validation errors, returning the row to its normal display state. - Show Feedback: Use
$this->toastSuccess()
to inform the user that the update was successful, showing both the old and new names for clarity.
- Trim Input:
- Add a public method
php
class Genres extends Component
{
use NotificationsTrait;
// sort properties
public $sortColumn = 'name';
public $sortDirection = 'asc';
// Property for the new genre input field
#[Validate('required|min:3|max:30|unique:genres,name', as: 'name for this genre')] // Add validation rules
public $newGenreName = '';
// Properties for edit a genre
public $editingGenreId = null;
#[Validate('required|min:3|max:30|unique:genres,name', as: 'name for this genre')]
public $editingGenreName = '';
// Method to handle column sorting
public function resort($column) { /* ... */ }
public function resetValues()
{
$this->reset('newGenreName', 'editingGenreName', 'editingGenreId');
$this->resetErrorBag();
}
public function editGenre(Genre $genre) { /* ... */ }
// Method to update a genre
public function updateGenre(Genre $genre)
{
// Trim the input before comparing and updating the genre name
$this->editingGenreName = trim($this->editingGenreName);
// If the name is not changed or is empty, do nothing
if (strtolower($this->editingGenreName) === strtolower($genre->name) || $this->editingGenreName === '') {
$this->resetValues();
return;
}
// Validate only the editingGenreName property based on its #[Validate] attribute
$this->validateOnly('editingGenreName');
// Store the old genre name for the toast message
$oldName = $genre->name;
// Update the genre name
$genre->update([
'name' => $this->editingGenreName
]);
// Reset the input field and error messages
$this->resetValues();
// Add a toast response
$this->toastSuccess(
"The genre <b><i>$oldName</i></b> has been updated to <b><i>$genre->name</i></b>",
[
'position' => 'top-right'
]
);
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
Delete a Genre (The "D" in CRUD)
WARNING
- Remember that we built in some integrity in our database tables
- If you delete a genre, all related records are deleted as well (as specified in the foreign key relation inside the records migration)
php
$table->foreignId('genre_id')->constrained()->onDelete('cascade')->onUpdate('cascade');
1
Finally, administrators need the ability to remove genres that are no longer needed or were created by mistake. Since deleting data is irreversible, it's crucial to ask for confirmation before proceeding. We'll implement a two-step deletion process using a confirmation dialog provided by our NotificationsTrait
.
- Initiate Confirmation: Clicking the "Delete" button (trash icon) will trigger a confirmation dialog asking the user if they are sure.
- Perform Deletion (if confirmed): If the user confirms the action in the dialog, a separate method will be called to actually delete the genre from the database.
- app/Livewire/Admin/Genres.php:
- Import the
Livewire\Attributes\On
attribute. This is needed to listen for Livewire events. - Add a public method
deleteConfirm(Genre $genre)
. This method is called when the trash icon is clicked.- It uses
$this->confirm()
(from theNotificationsTrait
) to display the modal dialog. - The first argument is the confirmation message (can include HTML).
- The second argument is an array. The crucial part is the
next
key:'onEvent' => 'delete-genre'
: This specifies that if the user clicks "Confirm" in the dialog, a Livewire event nameddelete-genre
should be dispatched.'genre' => $genre->id
: This includes the ID of the genre to be deleted as a parameter within the dispatched event.
- It uses
- Add another public method
deleteGenre(Genre $genre)
. This method will perform the actual deletion.- Add the
#[On('delete-genre')]
attribute above this method. This tells Livewire that this method should listen for thedelete-genre
event. When the event is received, Livewire will automatically call this method, passing the parameters included in the event (in this case, thegenre
ID, which gets resolved to aGenre
model via Route Model Binding). - Inside the method, call
$genre->delete()
to remove the genre from the database. - Show a success toast using
$this->toastSuccess()
to inform the user.
- Add the
- Import the
- resources/views/livewire/admin/genres.blade.php:
- Locate the "Delete" button (
<flux:button icon="trash">
) inside the@foreach
loop. - Add a
wire:click
directive to call thedeleteConfirm
method, passing the genre ID:wire:click="deleteConfirm({{ $genre->id }})"
. - Add a descriptive tooltip, e.g.,
tooltip="Delete {{ $genre->name }}"
.
- Locate the "Delete" button (
php
<?php
namespace App\Livewire\Admin;
use App\Models\Genre;
use App\Traits\NotificationsTrait;
use Livewire\Attributes\On;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Genres extends Component
{
use NotificationsTrait;
// sort properties
public $sortColumn = 'name';
public $sortDirection = 'asc';
// Property for the new genre input field
#[Validate('required|min:3|max:30|unique:genres,name', as: 'name for this genre')] // Add validation rules
public $newGenreName = '';
// Properties for edit a genre
public $editingGenreId = null;
#[Validate('required|min:3|max:30|unique:genres,name', as: 'name for this genre')]
public $editingGenreName = '';
// Method to handle column sorting
public function resort($column) { /* ... */ }
public function resetValues() { /* ... */ }
// Method to show deletion confirmation dialog
public function deleteConfirm(Genre $genre)
{
// Use the confirm helper from NotificationsTrait
$this->confirm(
"Are you sure you want to delete the genre <b><i>{$genre->name}</i></b>?", // Confirmation message
[
'next' => [ // Configuration for the action *after* confirmation
'onEvent' => 'delete-genre', // Livewire event to dispatch on confirm
'genre' => $genre->id // Parameter(s) for the event listener (pass genre ID)
]
]
);
}
// Method to actually delete the genre if confirmed
#[On('delete-genre')] // Listen for the 'delete-genre' event dispatched by the confirmation dialog
public function deleteGenre(Genre $genre) // Route Model Binding resolves ID from event data
{
// Delete the genre from the database
$genre->delete();
// Show success feedback
$this->toastSuccess(
"The genre <b><i>{$genre->name}</i></b> has been deleted.",
[
'position' => 'top-right'
]
);
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Exercises
Now that you have implemented the core CRUD functionality, let's refine the user experience and add features commonly found in administrative tables.
Make the genre name clickable
Currently, users must click the small pencil icon to edit a genre's name. While functional, a more user-friendly approach is to allow users to click directly on the genre name itself to enter edit mode.
Goal: Modify the table row so that clicking the displayed genre name (when not in edit mode) triggers the editGenre()
method, just like clicking the pencil icon does.
Add pagination to the table
Our current implementation fetches and displays all genres at once. While this works for a small list, it becomes inefficient and overwhelming if the shop has hundreds or thousands of genres. We need to implement pagination to break the list down into manageable pages.
Goal: Modify the component and view to display genres in paginated sets (e.g., 10 per page) and allow the user to select how many items they want to see per page using a dropdown in the table header, matching the appearance in the image below.