# Admin: genres
- Typical for admin pages is that an administrator can fully manage all the tables in the database
- Take for example the genres table: an administrator can add, change and delete a genre
- These operations are referred to as CRUD (C for create, R for read, U for Update and D for delete)
- In this chapter we will look at how to implement the CRUD for the genres table
REMARK
Remember that you can always reset the database to its initial state by running the command:
php artisan migrate:fresh
Copied!
1
DO THIS FIRST
- Before starting this chapter, make sure you have installed and configured SweetAlert2
# Preparation
# Create a Genres component
- Create a new Livewire component with the terminal command
php artisan make:livewire Admin/Genres
- app/Http/Livewire/Admin/Genres.php (the component class)
- resources/views/livewire/admin/genres.blade.php (the component view)
- Open the component class and change the layout to
layouts.vinylshop
class Genres extends Component { public function render() { return view('livewire.admin.genres') ->layout('layouts.vinylshop', [ 'description' => 'Manage the genres of your vinyl records', 'title' => 'Genres', ]); } }
Copied!
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# Add a new route
- Add a new get-route for the Genres to the routes/web.php file
- Update the navigation menu in resources/views/livewire/layout/nav-bar.blade.php
routes/web.php
resources/views/livewire/layout/nav-bar.blade.php
- Add the route in the admin group
- The URL is admin/genres (prefix is already set to admin)
- The view is admin/genres
- The route name is admin.genres (the group name is already set to admin.)
Route::view('/', 'home')->name('home'); Route::get('shop', Shop::class)->name('shop'); Route::view('contact', 'contact')->name('contact'); Route::get('itunes', Itunes::class)->name('itunes'); Route::view('playground', 'playground')->name('playground'); Route::middleware(['auth', 'active', 'admin'])->prefix('admin')->name('admin.')->group(function () { Route::redirect('/', '/admin/records'); Route::get('genres', Genres::class)->name('genres'); Route::get('records', [RecordController::class, 'index'])->name('records.index'); }); ...
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# Basic scaffolding for view
- Open resources/views/livewire/admin/genres.blade.php and replace the content with the following code:
livewire/admin/genres.blade.php
result
- Line 16: this section is hidden by default (the show/hide functionality will be added later)
<div> <x-tmk.section class="p-0 mb-4 flex flex-col gap-2"> <div class="p-4 flex justify-between items-start gap-4"> <div class="relative w-64"> <x-jet-input id="newGenre" type="text" placeholder="New genre" class="w-full shadow-md placeholder-gray-300"/> <x-phosphor-arrows-clockwise class="w-5 h-5 text-gray-200 absolute top-3 right-2 animate-spin"/> </div> <x-heroicon-o-information-circle class="w-5 text-gray-400 cursor-help outline-0"/> </div> <x-jet-input-error for="newGenre" class="m-4 -mt-4 w-full"/> <div style="display: none" class="text-sky-900 bg-sky-50 border-t p-4"> <x-tmk.list type="ul" class="list-outside mx-4 text-sm"> <li> <b>A new genre</b> can be added by typing in the input field and pressing <b>enter</b> or <b>tab</b>. Press <b>escape</b> to undo. </li> <li> <b>Edit a genre</b> by clicking the <x-phosphor-pencil-line-duotone class="w-5 inline-block"/> icon or by clicking on the genre name. Press <b>enter</b> to save, <b>escape</b> to undo. </li> <li> Clicking the <x-heroicon-o-information-circle class="w-5 inline-block"/> icon will toggle this message on and off. </li> </x-tmk.list> </div> </x-tmk.section> <x-tmk.section> <table class="text-center w-full border border-gray-300"> <colgroup> <col class="w-14"> <col class="w-20"> <col class="w-16"> <col class="w-max"> </colgroup> <thead> <tr class="bg-gray-100 text-gray-700 [&>th]:p-2 cursor-pointer"> <th> <span data-tippy-content="Order by id">#</span> <x-heroicon-s-chevron-up class="w-5 text-slate-400 inline-block"/> </th> <th> <span data-tippy-content="Order by # records"> <x-tmk.logo class="w-6 block mx-auto fill-gray-200 inline-block"/> </span> <x-heroicon-s-chevron-up class="w-5 text-slate-400 inline-block"/> </th> <th></th> <th class="text-left"> <span data-tippy-content="Order by genre">Genre</span> <x-heroicon-s-chevron-up class="w-5 text-slate-400 inline-block"/> </th> </tr> </thead> <tbody> <tr class="border-t border-gray-300 [&>td]:p-2"> <td>...</td> <td>...</td> <td> <div class="flex gap-1 justify-center [&>*]:cursor-pointer [&>*]:outline-0 [&>*]:transition"> <x-phosphor-pencil-line-duotone class="w-5 text-gray-300 hover:text-green-600"/> <x-phosphor-trash-duotone class="w-5 text-gray-300 hover:text-red-600"/> </div> </td> <td class="text-left cursor-pointer">... </td> </tr> </tbody> </table> </x-tmk.section> </div>
Copied!
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# Read all genres
- Open app/Http/Livewire/Admin/Genres.php and replace the content with the following code:
# Show all the genres in the table
Livewire/Admin/Genres.php
livewire/admin/genres.blade.php
result
- Line 4 - 5: add two properties
$orderBy
and$orderAsc
to the class - Line 9: select all genres with the number of records in each genre
- Line 10: order the results by the
$orderBy
property and the$orderAsc
property- If
$orderAsc
istrue
the results will->orderBy('name', 'asc')
- If
$orderAsc
isfales
the results will->orderBy('name', 'desc')
- If
- Line 12: send the results to the view
class Genres extends Component { // sort properties public $orderBy = 'name'; public $orderAsc = true; public function render() { $genres = Genre::withCount('records') ->orderBy($this->orderBy, $this->orderAsc ? 'asc' : 'desc') ->get(); return view('livewire.admin.genres', compact('genres')) ->layout('layouts.vinylshop', [ 'description' => 'Manage the genres of your vinyl records', 'title' => 'Genres', ]); } }
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Order the table by clicking on the column headers
Livewire/Admin/Genres.php
livewire/admin/genres.blade.php
result
- Line 8 - 14: click twice or more on the column header to change the order
- If the column is already ordered by the column header, the order will be reversed
- If the column is not ordered by the column header, the order will be ascending
- Line 13: set the
$orderBy
property to the column header that is clicked
class Genres extends Component { ... // resort the genres by the given column public function resort($column) { if ($this->orderBy === $column) { $this->orderAsc = !$this->orderAsc; } else { $this->orderAsc = true; } $this->orderBy = $column; } ... }
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Remove the unnecessary chevrons from the table headers
- Only the chevron of the column that is ordered by should be visible, the other chevrons should be hidden
- If the sort order is ascending, the chevron should point up
- If the sort order is descending, the chevron should point down (use the Tailwind class
rotate-180
)
livewire/admin/genres.blade.php
result
- Line 6 - 9: remove the class
inline-block
from the chevron (class="w-5 text-slate-400"
) and add two new dynamic classes inside the double quotes- Line 7 : if the
$orderAsc
property istrue
, add nothing else add the classrotate-180
- Line 8 : if the
$orderBy
property is equal to the column name, add the classinline-block
else add the classhidden
- Line 7 : if the
- Line 16 - 19: same logic as above, but
$orderBy
must be equalrecords_count
- Line 25 - 28: same logic as above, but
$orderBy
must be equalname
<thead> <tr class="bg-gray-100 text-gray-700 [&>th]:p-2 cursor-pointer"> <th wire:click="resort('id')"> <span data-tippy-content="Order by id">#</span> <x-heroicon-s-chevron-up class="w-5 text-slate-400 {{$orderAsc ?: 'rotate-180'}} {{$orderBy === 'id' ? 'inline-block' : 'hidden'}} "/> </th> <th wire:click="resort('records_count')"> <span data-tippy-content="Order by # records"> <x-tmk.logo class="w-6 block mx-auto fill-gray-200 inline-block"/> </span> <x-heroicon-s-chevron-up class="w-5 text-slate-400 {{$orderAsc ?: 'rotate-180'}} {{$orderBy === 'records_count' ? 'inline-block' : 'hidden'}} "/> </th> <th></th> <th wire:click="resort('name')" class="text-left"> <span data-tippy-content="Order by genre">Genre</span> <x-heroicon-s-chevron-up class="w-5 text-slate-400 {{$orderAsc ?: 'rotate-180'}} {{$orderBy === 'name' ? 'inline-block' : 'hidden'}} "/> </th> </tr> </thead>
Copied!
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
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
# Create a new genre
# Add a new genre
Livewire/Admin/Genres.php
livewire/admin/genres.blade.php
result
- Line 4: add a property
$newGenre
to the class - Line 7- 8: define the validation rules for the
$newGenre
property- The name is required, minimum 3 characters and maximum 30 characters long
- The name must be unique in the
genres
table (see: info about unique validation rule (opens new window))
- Line 12 - 20: create a new genre
- Line 15: validate the
$newGenre
property before creating the genre - Line 17 - 19: create a new genre with the
$newGenre
property as the name of the genre
(Tip: the PHPtrim()
function removes all whitespace from the beginning and end of a string)
- Line 15: validate the
class Genre extends Component { ... public $newGenre; // validation rules protected $rules = [ 'newGenre' => 'required|min:3|max:30|unique:genres,name', ]; // create a new genre public function createGenre() { // validate the new genre name $this->validate(); // create the genre Genre::create([ 'name' => trim($this->newGenre), ]); } ... }
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
REMARKS
Validation
- Because we have only one input field that is defered, there is no need to do real-time validation (like we did earlier in this course)
- The validation is taken care of by the
createGenre()
method
- The validation is taken care of by the
Create vs save
- We use the
Genre::create()
method to create a new genre.- This method is not part of the Eloquent ORM.
- It is a static method that is part of the Model class.
- It is a convenience method that creates a new instance of the model and saves it to the database in a single step.
- It is equivalent to the following code:
$genre = new Genre(); $genre->name = $this->newGenre; $genre->save();
Copied!1
2
3
- One important difference between
Genre::create()
and$genre->save()
is that theGenre::create()
is more secure because it passes the$fillable
(or$guarded
) properties of the model where$genre->save()
does not.
# Clear the input field
- After a new genre is created, the input field should be cleared
- This can be done by setting the
$newGenre
property to an empty string - And also the
resetErrorBag()
method or theresetValidation()
method must be called to clear the validation errors (if there are any)
- This can be done by setting the
- When we click on the Esc key, the input field should be cleared as well
- Because we have to do this in two places, we will create a methode
resetNewGenre()
to do this
Livewire/Admin/Genres.php
livewire/admin/genres.blade.php
result
- Line 18: call the
resetNewGenre()
method after creating the genre - Line 2 - 6: create a new method
resetNewGenre()
to clear the$newGenre
property and the validation errors- Line 4: reset the
$newGenre
property to its default value (an empty string) - Line 5: call the
resetErrorBag()
method to clear all the validation errors
- Line 4: reset the
// reset $newGenre and validation public function resetNewGenre() { $this->reset('newGenre'); $this->resetErrorBag(); } // create a new genre public function createGenre() { // validate the new genre name $this->validate(); // create the genre Genre::create([ 'name' => trim($this->newGenre), ]); // reset $newGenre $this->resetNewGenre(); }
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Add a toast response
- When a new genre is created, we want to show a toast message to the user
- We will use the SweetAlert2 JavaScript library for this
- After a new genre is created, we will emit a browser event with the name
swal:toast
and pass the name of the new genre as the html message for out toast
Livewire/Admin/Genres.php
result
- Line 7: add the newly created genre to the variable
$genre
so we can use it in our toast message
(ReplaceGenre::create([...]);
with$genre = Genre::create([...]);
) - Line 13 - 16: emmit a browser event with the name
swal:toast
- Line 14: set the background color to
success
(a light green color) - Line 15: include the name of the new genre in the toast message
- Line 14: set the background color to
// create a new genre public function createGenre() { // validate the new genre name $this->validate(); // create the genre $genre = Genre::create([ 'name' => trim($this->newGenre), ]); // reset $newGenre $this->resetNewGenre(); // toast $this->dispatchBrowserEvent('swal:toast', [ 'background' => 'success', 'html' => "The genre <b><i>{$genre->name}</i></b> has been added", ]); }
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Update the spinner icon
- The spinner icon (the subtle gray icon in the right corner of the input field) should only be visible when the
createGenre()
method is running - We can do this by using the
wire:loading
directive like we did before- Line 9:
wire:loading
runs every time one of the methods is called - Line 10: to make it more specific, we can use
wire:target
to specify ONLY ONE method that should be watched, in our case we want to watch thecreateGenre()
method - Line 11: change the color of
text-gray-200
totext-gray-500
so that the spinner stands out a little more
- Line 9:
<div class="relative w-64"> <x-jet-input id="newGenre" type="text" placeholder="New genre" wire:model.defer="name" wire:keydown.enter="createGenre()" wire:keydown.tab="createGenre()" wire:keydown.escape="resetNewGenre()" class="w-full shadow-md placeholder-gray-300"/> <x-phosphor-arrows-clockwise wire:loading wire:target="createGenre" class="w-5 h-5 text-gray-500 absolute top-3 right-2 animate-spin"/> </div>
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# Show/hide the info section
- The info section should toggle when the Info button, in the top right corner, is clicked
- Because this ia a pure client-side action, Alpine is the perfect tool for this
livewire/admin/genres.blade.php
result
- Line 6: create a new Alpine component with
x-data
with a propertyopen
that is set tofalse
- Line 9: every click on the info icon will toggle the
open
property - Line 14: the section is only visible when the
open
property istrue
<x-tmk.section x-data="{ open: false }" class="p-0 mb-4 flex flex-col gap-2"> <div class="m-4 flex justify-between items-start gap-4"> <div class="relative w-64"> ... </div> <x-heroicon-o-information-circle @click="open = !open" class="w-5 text-gray-600 cursor-help outline-0"/> </div> <x-jet-input-error for="newGenre" class="m-4 -mt-4 w-full"/> <div x-show="open" style="display: none" class="text-sky-900 bg-sky-50 border-t p-4"> <x-tmk.list type="ul" class="list-outside mx-4 text-sm"> ... </x-tmk.list> </div> </x-tmk.section>
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Update a genre
- Because we have only one input field, we can make it inline editable
- In edit mode:
- hide the buttons in the third column
- replace the name in the last column with an input field
# Enter edit mode
Livewire/Admin/Genres.php
livewire/admin/genres.blade.php
result
- Line 6: add a new property
$editGenre
:- This property contains an array with the keys
id
andname
and is initialized with empty values (null
)
- This property contains an array with the keys
- Line 11 - 17: when the Pencil icon is clicked, the
editGenre()
method is called and theid
of the genre is passed as a parameter- Line 11: Use "route model binding" to get the full genre as the id is passed as a parameter
- Line 13 - 16: set the
$editGenre
property to theid
andname
of the selected genre
class Genres extends Component { ... public $newGenre; public $editGenre = ['id' => null, 'name' => null]; ... // edit the value of $editGenre (show inlined edit form) public function editExistingGenre(Genre $genre) { $this->editGenre = [ 'id' => $genre->id, 'name' => $genre->name, ]; } public function render() { ... } }
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Update the genre
- The functionality to update the genre is almost the same as the functionality to create a genre
- Click the Enter or Tab key to save the changes
- Click the Escape key to cancel the changes
- Validate the input field before saving the changes
- Show an error message when the validation fails
- Show a success toast when the genre is updated
Livewire/Admin/Genres.php
livewire/admin/genres.blade.php
result 1
result 2
- Line 8: use the same validation rules as for the
newGenre
property - Line 15 - 19: this function resets the
$editGenre
property to its initial value and clears the validation errors- Don't use the
reset()
method without a parameter, because this will reset all properties (inc. the$orderBy
and$orderAsc
properties)
- Don't use the
- Line 23 - 38:
- Validate the input field before saving the changes and show an error message when the validation fails
- Save the original (not yet updated) name of the genre in a variable
$oldName
- Update the genre in the database with
$genre->update([...])
- call the
resetEditGenre()
method to reset the$editGenre
property and the validation errors - Dispatch a success toast
class Genres extends Component { ... // validation rules protected $rules = [ 'newGenre' => 'required|min:3|max:30|unique:genres,name', 'editGenre.name' => 'required|min:3|max:30|unique:genres,name', ]; // reset $newGenre and validation public function resetNewGenre() { ... } // reset $editGenre and validation public function resetEditGenre() { $this->reset('editGenre'); $this->resetErrorBag(); } // update an existing genre public function updateGenre(Genre $genre) { $this->validate(); $oldName = $genre->name; $genre->update([ 'name' => trim($this->editGenre['name']), ]); $this->resetEditGenre(); $this->dispatchBrowserEvent('swal:toast', [ 'background' => 'success', 'html' => "The genre <b><i>{$oldName}</i></b> has been updated to <b><i>{$genre->name}</i></b>", ]); } ... }
Copied!
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
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
# Modify the validation methods
- Line 16: replace the
validate()
method with thevalidateOnly('editGenre.name');
method - Line 24: replace the
validate()
method with thevalidateOnly('newGenre');
method
class Genres extends Component { ... // validation rules protected $rules = [ 'newGenre' => 'required|min:3|max:30|unique:genres,name', 'editGenre.name' => 'required|min:3|max:30|unique:genres,name', ]; ... // update an existing genre public function updateGenre(Genre $genre) { $this->validateOnly('editGenre.name'); ... } // create a new genre public function createGenre() { // validate the new genre name $this->validateOnly('newGenre'); ... } ... }
Copied!
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
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
# Update the validation rule for the updateGenre method
IMPORTANT
There are actually 3 ways to validate a unique value:
unique:table,column
: the value for thecolumn
must be unique in the whole tableunique:table
: the same as above, but the column name is the same as the field name e.g.'name' => 'required|min:3|max:30|unique:genres,name',
can be written as'name' => 'required|min:3|max:30|unique:genres',
'genre.name' => 'required|min:3|max:30|unique:genres,name',
can NOT be written as'genre.name' => 'required|min:3|max:30|unique:genres',
unique:table,column,idColumn
: the value must be unique in the whole table, except for the current record e.g.'genre.name' => 'required|min:3|max:30|unique:genres,name,' . $this->edit['id'],
Use the rules()
methode instead of the $rules
property
- The
$rules
property can only be used for static validation rules, not for dynamic rules like we need for the update method - The
rules()
method can be used for static AND dynamic rules - Because we have a dynamic rule (we have to add
$this->edit['id']
to the rule), we have to change our validation rules from:
protected $rules = [ // validation rules here ];
Copied!
1
2
3
2
3
- to:
public function rules() { return [ // validation rules here ]; }
Copied!
1
2
3
4
5
6
2
3
4
5
6
Livewire/Admin/Genres.php
result
- Update the validation:
- Replace the
$rules
property with arules()
method - Add the
id
of the current record to the validation rule
- Replace the
// validation rules public function rules() { return [ 'newGenre' => 'required|min:3|max:30|unique:genres,name', 'editGenre.name' => 'required|min:3|max:30|unique:genres,name,' . $this->editGenre['id'], ]; }
Copied!
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# Update validation message
- If you want to keep the default Laravel validation messages, but just customize the
:attribute
portion of the message, you can specify custom attribute names using the$validationAttributes
property
Livewire/Admin/Genres.php
result
- Line 12: change the default validation attribute name from
editGenre.name
togenre name
// validation rules public function rules() { return [ 'newGenre' => 'required|min:3|max:30|unique:genres,name', 'editGenre.name' => 'required|min:3|max:30|unique:genres,name,' . $this->editGenre['id'], ]; } // validation attributes protected $validationAttributes = [ 'editGenre.name' => 'genre name', ];
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# Custom validation messages (optional)
- It's also possible to customize the entire validation message using the
$messages
property - You have to write a custom message for each rule that you want to customize
Livewire/Admin/Genres.php
result
- In the example below, we update the validation messages for all rules that are used in the
rules()
method - If you do so, you can remove the
$validationAttributes
property because the custom messages override the default messages
// validation rules public function rules() { return [ 'newGenre' => 'required|min:3|max:30|unique:genres,name', 'editGenre.name' => 'required|min:3|max:30|unique:genres,name,' . $this->editGenre['id'], ]; } // validation attributes /*protected $validationAttributes = [ 'editGenre.name' => 'genre name', ];*/ // custom validation messages protected $messages = [ 'newGenre.required' => 'Please enter a genre name.', 'newGenre.min' => 'The new name must contains at least 3 characters and no more than 30 characters.', 'newGenre.max' => 'The new name must contains at least 3 characters and no more than 30 characters.', 'newGenre.unique' => 'This name already exists.', 'editGenre.name.required' => 'Please enter a genre name.', 'editGenre.name.min' => 'This name is too short (must be between 3 and 30 characters).', 'editGenre.name.max' => 'This name is too long (must be between 3 and 30 characters)', 'editGenre.name.unique' => 'This name is already in use.', ];
Copied!
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Delete a genre
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)
$table->foreignId('genre_id')->constrained()->onDelete('cascade')->onUpdate('cascade');
Copied!
1
- Click on the Trash icon to delete a genre
- It's a good practice always to ask the user for a confirmation that he really wants to delete some (database) data
- You can do this with the vanilla JavaScript
confirm()
(opens new window) function
WARNING
If you don't want to lose any data, test this functionality with a newly created genres (that is not linked to any record in the database), e.g. 'afrobeat'!
Livewire/Admin/Genres.php
livewire/admin/genres.blade.php
result 1
result 2
- Line 6: add a
deleteGenre()
method that will be called when the user clicks on the Trash icon
Use route model binding to get the genre that has to be deleted - Line 8: delete the genre
- Line 9 - 12: show a toast message that the genre is deleted
class Genres extends Component { ... // delete a genre public function deleteGenre(Genre $genre) { $genre->delete(); $this->dispatchBrowserEvent('swal:toast', [ 'background' => 'success', 'html' => "The genre <b><i>{$genre->name}</i></b> has been deleted", ]); } ... }
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Improve the UX
There are some things that can be improved in the user experience:
- In edit mode, the cursor should be in the input field (now the user has to click in the input field to start typing)
- Create and edit a genre: when clicking the Escape, Return or Tab key, the input field is still editable, and we have to wait for the server response before everything is back to normal
We want to temporary disable the input field while the server is processing the request - The default JavaScript
confirm()
dialog is not very pretty
We can use the SweetAlert2 dialog to create a more beautiful dialog
# Set the cursor in the input field
- Line 4: initialize a new Alpine component
- Line 5: add
x-init="$el.focus()"
to input field$el
refers to the element itself (the input field)focus()
(opens new window) is a regular JavaScript function to sets the focus on the input field
<td> <div class="flex flex-col text-left"> <x-jet-input id="edit_{{ $genre->id }}" type="text" x-data="" x-init="$el.focus()" wire:model.defer="editGenre.name" wire:keydown.enter="updateGenre({{ $genre->id }})" wire:keydown.tab="updateGenre({{ $genre->id }})" wire:keydown.escape="resetEditGenre()" class="w-48"/> <x-jet-input-error for="editGenre.name" class="mt-2"/> </div> </td>
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# Disable the input field
edit genre input field
new genre input field
- Line 6 - 8: set the attribute
disabled
(opens new window) in the input field totrue
<td> <div class="flex flex-col text-left"> <x-jet-input id="edit_{{ $genre->id }}" type="text" x-data="" x-init="$el.focus()" @keydown.enter="$el.setAttribute('disabled', true);" @keydown.tab="$el.setAttribute('disabled', true);" @keydown.esc="$el.setAttribute('disabled', true);" wire:model.defer="editGenre.name" wire:keydown.enter="updateGenre({{ $genre->id }})" wire:keydown.tab="updateGenre({{ $genre->id }})" wire:keydown.escape="resetEditGenre()" class="w-48"/> <x-jet-input-error for="editGenre.name" class="mt-2"/> </div> </td>
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Use SweetAlert2 for the confirmation dialog
# Basic version
- Replace the vanilla JavaScript
confirm()
dialog box with a SweetAlert2 dialog
livewire/admin/genres.blade.php
Livewire/Admin/Genres.php
result
- Line 9 - 19: dispatch the browser event
swal:confirm
and add some properties to add/overwrite the default settings for the SweetAlert2 dialog - Line 13 - 17: this object contains the properties to create a new browser event for the triggering the actual delete action
<td x-data=""> @if($editGenre['id'] !== $genre->id) <div class="flex gap-1 justify-center [&>*]:cursor-pointer [&>*]:outline-0 [&>*]:transition"> <x-phosphor-pencil-line-duotone wire:click="editExistingGenre({{ $genre->id }})" class="w-5 text-gray-300 hover:text-green-600"/> <x-phosphor-trash-duotone @click="$dispatch('swal:confirm', { title: 'Delete genre?', cancelButtonText: 'NO!', confirmButtonText: 'YES DELETE THIS GENRE', next: { event: 'delete-genre', params: { id: {{ $genre->id }} } } });" class="w-5 text-gray-300 hover:text-red-600"/> </div> @endif </td>
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Advanced version
- We will show a different dialog for genres with records and genres without records
- If the genre has no records, we will show a simple, white dialog
- If the genre has records, we will show a red dialog with an extra warning message and an icon
livewire/admin/genres.blade.php
result
- Line 10: add the genre name to the title of the dialog
- If the genre has records:
- Line 11: add the warning icon
- Line 12: give the background of the dialog a red color
- Line 15: add an extra warning message to the dialog
- Line 16: color the text of the warning message red
<td x-data=""> @if($editGenre['id'] !== $genre->id) <div class="flex gap-1 justify-center [&>*]:cursor-pointer [&>*]:outline-0 [&>*]:transition"> <x-phosphor-pencil-line-duotone wire:click="editExistingGenre({{ $genre->id }})" class="w-5 text-gray-300 hover:text-green-600"/> <x-phosphor-trash-duotone @click="$dispatch('swal:confirm', { title: 'Delete {{ $genre->name }}?', icon: '{{ $genre->records_count > 0 ? 'warning' : '' }}', background: '{{ $genre->records_count > 0 ? 'error' : '' }}', cancelButtonText: 'NO!', confirmButtonText: 'YES DELETE THIS GENRE', html: '{{ $genre->records_count > 0 ? '<b>ATTENTION</b>: you are going to delete <b>' . $genre->records_count . ' ' . Str::plural('record', $genre->records_count) . '</b> at the same time!' :'' }}', color: '{{ $genre->records_count > 0 ? 'red' : '' }}', next: { event: 'delete-genre', params: { id: {{ $genre->id }} } } });" class="w-5 text-gray-300 hover:text-red-600"/> </div> @endif </td>
Copied!
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
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
# EXERCISES:
# 1: Make the genre name clickable
- For now, only the Pencil icon is clickable to edit the genre name
- Make the genre name clickable as well to edit the genre
# 2: Add a spinner to the update input fields
- Add, just like in the create input field, a spinner to the update input fields
- The spinner is only visible when the server is processing
updateGenre()
method or theresetEditGenre()
method
# 3: Don't update the genre when the name has not changed
- When just clicking the Enter key without changing the name, you will also get a toast message that the genre has been updated
- Change the code so that you only update the genre when the new value is different from the old value
# 4: Add pagination to the table
- Add a new public property
$perPage
with a default value of10
to theGenres
class - Use this property to limit the number of genres that are shown in the table
- Append a fifth column to the table and add a select element to the table header to select the number of genres that are shown per page