# 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

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

# Basic scaffolding for view

  • Open resources/views/livewire/admin/genres.blade.php and replace the content with the following code:
  • 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

# 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

  • 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 is true the results will ->orderBy('name', 'asc')
    • If $orderAsc is fales the results will ->orderBy('name', 'desc')
  • 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

# Order the table by clicking on the column headers

  • 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

# 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)
  • 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 is true, add nothing else add the class rotate-180
    • Line 8 : if the $orderBy property is equal to the column name, add the class inline-block else add the class hidden
  • Line 16 - 19: same logic as above, but $orderBy must be equal records_count
  • Line 25 - 28: same logic as above, but $orderBy must be equal name





 
 
 
 






 
 
 
 





 
 
 
 




<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

# Create a new genre

# Add a new genre

  • Line 4: add a property $newGenre to the class
  • Line 7- 8: define the validation rules for the $newGenre property
  • 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 PHP trim() function removes all whitespace from the beginning and end of a string)



 

 
 
 
 

 
 
 
 
 
 
 
 
 
 




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

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

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 the Genre::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 the resetValidation() method must be called to clear the validation errors (if there are any)
  • 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
  • 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
 
 
 
 
 
 










 
 


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

# 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
  • Line 7: add the newly created genre to the variable $genre so we can use it in our toast message
    (Replace Genre::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






 




 
 
 
 
 


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

# 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 the createGenre() method
    • Line 11: change the color of text-gray-200 to text-gray-500 so that the spinner stands out a little more








 
 
 


<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

# 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
  • Line 6: create a new Alpine component with x-data with a property open that is set to false
  • Line 9: every click on the info icon will toggle the open property
  • Line 14: the section is only visible when the open property is true

 






 




 








<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

# 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

  • Line 6: add a new property $editGenre:
    • This property contains an array with the keys id and name and is initialized with empty values (null)
  • Line 11 - 17: when the Pencil icon is clicked, the editGenre() method is called and the id 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 the id and name 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

# 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
  • 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)
  • 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

# Modify the validation methods

  • Line 16: replace the validate() method with the validateOnly('editGenre.name'); method
  • Line 24: replace the validate() method with the validateOnly('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

# 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 table
  • unique: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
  • to:
public function rules()
{
    return [
        // validation rules here
    ];
}
Copied!
1
2
3
4
5
6
  • Update the validation:
    • Replace the $rules property with a rules() method
    • Add the id of the current record to the validation rule





 



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

# 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
  • Line 12: change the default validation attribute name from editGenre.name to genre 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

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

# 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'!

  • 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

# 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

# Disable 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()"
                     @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

# Use SweetAlert2 for the confirmation dialog

# Basic version

  • Replace the vanilla JavaScript confirm() dialog box with a SweetAlert2 dialog
  • 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

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

# 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 the resetEditGenre() 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 of 10 to the Genres 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
    Pagination
Last Updated: 12/6/2022, 8:55:41 PM