Appearance
Admin: genres
PODCAST
- 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 forread
, U forUpdate
and D fordelete
) - 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:
bash
php artisan migrate:fresh --seed
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 livewire:make Admin/Genres
- app/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
php
<?php
namespace App\Livewire\Admin;
use Livewire\Attributes\Layout;
use Livewire\Component;
class Genres extends Component
{
#[Layout('layouts.vinylshop', ['title' => 'Genres', 'description' => 'Manage the genres of your vinyl records',])]
public function render()
{
return view('livewire.admin.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
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/components/layout/nav.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.)
php
Route::middleware(['auth', Admin::class, ActiveUser::class])->prefix('admin')->name('admin.')->group(function () {
Route::redirect('/', '/admin/records');
Route::get('genres', Genres::class)->name('genres');
Route::get('records', Demo::class)->name('records');
});
...
1
2
3
4
5
6
2
3
4
5
6
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)
php
<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-input id="newGenre" type="text" placeholder="New genre"
class="w-full shadow-md placeholder-gray-300"/>
<x-phosphor-arrows-clockwise
class="size-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-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-24">
<col class="w-24">
<col class="w-16">
<col class="w-max">
</colgroup>
<thead>
<tr class="bg-gray-100 text-gray-700 [&>th]:p-2">
<th role="button">
#
</th>
<th role="button">
<x-tmk.logo class="size-6 fill-gray-200 w-full"/>
</th>
<th></th>
<th class="text-left" role="button">
Genre
</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>
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
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
Read all genres
- Open app/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
$sortColumn
and$sortOrder
to the class - Line 10: select all genres with the number of records in each genre
- Line 11: order the results by the
$sortColumn
property and the$sortOrder
property- If
$sortOrder
isasc
the results will->orderBy('name', 'asc')
- If
$sortOrder
isdesc
the results will->orderBy('name', 'desc')
- If
- Line 13: send the results to the view
php
class Genres extends Component
{
// sort properties
public $sortColumn = 'name';
public $sortOrder = 'asc';
#[Layout('layouts.vinylshop', ['title' => 'Genres', 'description' => 'Manage the genres of your vinyl records',])]
public function render()
{
$genres = Genre::withCount('records')
->orderBy($this->sortColumn, $this->sortOrder)
->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 on the column headers
- Line 7 - 12: 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 10: set the
$sortColumn
property to the column header that is clicked - Line 11: set the
$sortOrder
property to'asc'
php
class Genres extends Component
{
...
public function resort($column)
{
if ($this->sortColumn === $column) {
$this->sortOrder = $this->sortOrder === 'asc' ? 'desc' : 'asc';
} else {
$this->sortColumn = $column;
$this->sortOrder = 'asc';
}
}
...
}
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
CHEVRONS
Let’s indicate the sorted column to the user by adding chevrons (⌃ or ⌄) next to the column name.
- Column is ordered from low to high: show a chevron pointing up
- Column is ordered from high to low: show a chevron pointing down
- Column is not ordered: show an up and down chevron
Because we have to do this in three places, we will create a new, reusable component for this
Create a sortable-header component
- Create a new table folder inside resources/views/components/tmk
- Create a new file sortable-header.blade.php inside the resources/views/components/tmk/table folder
- Copy the following code into the sortable-header.blade.php file
- Line 1 - 5: the component has three properties:
sortColumn
: the column that is ordered bysortOrder
: the order of the column (asc
ordesc
)position
: the position of the content (left
,center
orright
)
- Line 7 - 14: the
$style
variable contains the Tailwind classes for the different positions (default isleft
) - Line 17: additional classes will be added to the
div
element - Line 19 - 27:
$attributes->wire('click')->value
contains the method that is called when the column header is clicked (e.g.resort('id')
)- If the value contains the name of the column that is ordered by, the up or the down chevron will be shown
- If the value does not contain the name of the column that is ordered by, the up-down chevron will be shown
- Line 1 - 5: the component has three properties:
php
@props([
'sortColumn' => null,
'sortOrder' => 'asc',
'position' => 'left',
])
@php
$style = [
'left' => 'flex items-center gap-1',
'center' => 'flex items-center gap-1 justify-center translate-x-3',
'right' => 'flex items-center gap-1 justify-end',
];
$style = $style[$position] ?? $style['left'];
@endphp
<th role="button">
<div {{ $attributes->merge(['class' => $style]) }}>
{{ $slot }}
@if(str_contains($attributes->wire('click')->value, $sortColumn))
@if(strtolower($sortOrder) === 'asc')
<x-heroicon-s-chevron-up class="size-5 text-slate-600 inline-block"/>
@else
<x-heroicon-s-chevron-down class="size-5 text-slate-600 inline-block"/>
@endif
@else
<x-heroicon-s-chevron-up-down class="size-5 text-slate-400 inline-block"/>
@endif
</div>
</th>
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
Replace the table headers with the new component
- Replace all
th
elements in thethead
section with the newx-tmk.table.sortable-header
component- The
wire:click
directive is the same as before - The
position
attribute is set tocenter
for first and second column - The
sortColumn
andsortOrder
properties are passed to the component
- The
IMPORTANT: because the sortColumn
and sortOrder
properties contains dynamic values, you have to use the colon syntax to bind the properties to the component!
(See chapter Reusable components)
php
<thead>
<tr class="bg-gray-100 text-gray-700 [&>th]:p-2">
<x-tmk.table.sortable-header wire:click="resort('id')"
position="center" :sortColumn="$sortColumn" :sortOrder="$sortOrder">
id
</x-tmk.table.sortable-header>
<x-tmk.table.sortable-header wire:click="resort('records_count')"
position="center" :sortColumn="$sortColumn" :sortOrder="$sortOrder">
<x-tmk.logo class="size-6 fill-gray-200"/>
</x-tmk.table.sortable-header>
<th></th>
<x-tmk.table.sortable-header wire:click="resort('name')"
:sortColumn="$sortColumn" :sortOrder="$sortOrder">
Genre
</x-tmk.table.sortable-header>
</tr>
</thead>
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
Create a new genre
Add a new genre
- Line 17: add a property
$newGenre
to the class - Line 16: define the validation rules for the
$newGenre
property using Livewire's#[Validate]
attribute- 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)
- Line 20 - 28: create a new genre
- Line 23: validate the
$newGenre
property before creating the genre - Line 25 - 27: 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 23: validate the
php
<?php
namespace App\Livewire\Admin;
use App\Models\Genre;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Component;
class Genres extends Component
{
// sort properties
public $sortColumn = 'name';
public $sortOrder = 'asc';
#[Validate('required|min:3|max:30|unique:genres,name')]
public $newGenre;
// create a new genre
public function create()
{
// validate the new genre name
$this->validateOnly('newGenre');
// create the genre
Genre::create([
'name' => trim($this->newGenre),
]);
}
// resort the genres by the given column
public function resort($column){ ... }
#[Layout('layouts.vinylshop', ['title' => 'Genres', 'description' => 'Manage the genres of your vinyl records',])]
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
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
REMARKS
Validation
- Because we have only one input field that is defered, there is no need to do real-time validation
- The validation is taken care of by the
create()
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:php
$genre = new Genre(); $genre->name = $this->newGenre; $genre->save();
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
resetValues()
to do this
- Line 21: call the
resetValues()
method after creating the genre - Line 5 - 9: create a new method
resetValues()
to clear the$newGenre
property and the validation errors- Line 7: reset the
$newGenre
property to its default value (an empty string) - Line 8: call the
resetErrorBag()
method to clear all the validation errors
- Line 7: reset the
php
#[Validate('required|min:3|max:30|unique:genres,name')]
public $newGenre;
// reset all the values and error messages
public function resetValues()
{
$this->reset('newGenre');
$this->resetErrorBag();
}
// create a new genre
public function create()
{
// validate the new genre name
$this->validateOnly('newGenre');
// create the genre
Genre::create([
'name' => trim($this->newGenre),
]);
// reset $newGenre
$this->resetValues();
}
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
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 (from the SweetAlertTrait) and pass the name of the new genre as the html message for out toast
- Line 3: use the
SweetAlertTrait
to the class - Line 13: 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 19: emmit an event with the name
swalToast
method from theSweetAlertTrait
trait- first parameter: the message of the toast (we will use the name of the new genre) inside a double-quoted string
- second parameter: this can be omitted because the default value is
success
(light green background color)
php
class Genres extends Component
{
use SweetAlertTrait;
...
// create a new genre
public function create()
{
// validate the new genre name
$this->validateOnly('newGenre');
// create the genre
$genre = Genre::create([
'name' => trim($this->newGenre),
]);
// reset $newGenre
$this->resetValues();
// show a success toast
$this->swalToast("The genre <b><i>$genre->name</i></b> has been added");
}
...
}
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
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
create()
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 thecreate()
method - Line 11: add the class
hidden
and change the color oftext-gray-200
totext-gray-500
so that the spinner stands out a little more
- Line 9:
php
<div class="relative w-64">
<x-input id="newGenre" type="text" placeholder="New genre"
wire:model="newGenre"
wire:keydown.enter="create()"
wire:keydown.tab="create()"
wire:keydown.escape="resetValues()"
class="w-full shadow-md placeholder-gray-300"/>
<x-phosphor-arrows-clockwise
wire:loading
wire:target="create"
class="hidden size-5 text-gray-500 absolute top-3 right-2 animate-spin"/>
</div>
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
- Line 2: 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
- Line 15: add a transition effect to the section
php
<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-input-error for="newGenre" class="m-4 -mt-4 w-full"/>
<div
x-show="open"
x-transition
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>
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 validation messages
- In the default validation messages, the 'title case" name of the field is shown in the error message
- The variable
$newGenre
will be translated tonew genre
in the error message - You have two options to override the default validation name by adding the property
as
orattribute
to the#[Validate]
attribute
php
#[Validate(
'required|min:3|max:30|unique:genres,name',
attribute: 'name for this genre',
)]
public $newGenre;
1
2
3
4
5
2
3
4
5
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 17: 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 12 - 16: define the validation rules for
$editGenre
editGenre.name
is required, minimum 3 characters and maximum 30 characters longeditGenre.id
needs no validation because it is a hidden field- and, as for the creation validation message, we will replace the default validation name
edit genre name
withname for this genre
- Line 23 - 29: when the Pencil icon in the frontend is clicked, the
edit()
method is called and theid
of the genre is passed as a parameter- Line 23: Use "route model binding" to get the full genre as the
id
is passed as a parameter - Line 25 - 28: set the
$editGenre
property to theid
andname
of the selected genre
- Line 23: Use "route model binding" to get the full genre as the
php
class Genres extends Component
{
use SweetAlertTrait;
// sort properties
public $sortColumn = 'name';
public $sortOrder = 'asc';
#[Validate('required|min:3|max:30|unique:genres,name', as: 'name for this genre',)]
public $newGenre;
#[Validate([
'editGenre.name' => 'required|min:3|max:30|unique:genres,name',
], as: [
'editGenre.name' => 'name for this genre',
])]
public $editGenre = ['id' => null, 'name' => null];
// reset all the values and error messages
public function resetValues() { ... }
// copy the selected genre to $editGenre
public function edit(Genre $genre)
{
$this->editGenre = [
'id' => $genre->id,
'name' => $genre->name,
];
}
...
}
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
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
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
- If the new name is the same as the original name, do nothing
- If the new name is different from the original name, update the genre in the database
- 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 17: add
editGenre
as a parameter to thereset()
method, so we can reuse theresetValues()
method for both the creation and the update of a genre - Line 24 - 43:
- Line 27: replace the value of
$editGenre['name']
with the trimmed value of$editGenre['name']
- Line 29 - 32: if the name is not changed, do nothing
(Tip: use the PHPstrtolower()
function to compare the names in a case-insensitive way, else 'Afrobeat' and 'afrobeat' are not the same) - Line 33: validate the input field before saving the changes and show an error message when the validation fails
- Line 34: save the original (not yet updated) name of the genre in a variable
$oldName
- Line 35 - 37: update the genre in the database with
$genre->update([...])
- Line 38: call the
resetValues()
method to reset the$editGenre
property and the validation errors - Line 39: dispatch a success toast
- Line 27: replace the value of
php
class Genres extends Component
{
// sort properties
public $sortColumn = 'name';
public $sortOrder = 'asc';
#[Validate('required|min:3|max:30|unique:genres,name', as: 'name for this genre',)]
public $newGenre;
#[Validate(['editGenre.name' => 'required|min:3|max:30|unique:genres,name',], as: ['editGenre.name' => 'name for this genre',
])]
public $editGenre = ['id' => null, 'name' => null];
// reset all the values and error messages
public function resetValues()
{
$this->reset('newGenre', 'editGenre');
$this->resetErrorBag();
}
// copy the selected genre to $editGenre
public function edit(Genre $genre) { ... }
// update the genre
public function update(Genre $genre)
{
$this->editGenre['name'] = trim($this->editGenre['name']);
// if the name is not changed, do nothing
if(strtolower($this->editGenre['name']) === strtolower($genre->name)) {
$this->resetValues();
return;
}
$this->validateOnly('editGenre.name');
$oldName = $genre->name;
$genre->update([
'name' => trim($this->editGenre['name']),
]);
$this->resetValues();
$this->swalToast("The genre <b><i>{$oldName}</i></b> has been updated to <b><i>{$genre->name}</i></b>");
}
...
}
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
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
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)
php
$table->foreignId('genre_id')->constrained()->onDelete('cascade')->onUpdate('cascade');
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 Livewire
wire:confirm
function (basic version) or with the SweetAlert2 dialog (more advanced version)
Basic version
- Line 6: add a
delete()
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: show a toast message that the genre is deleted
php
class Genres extends Component
{
...
// delete a genre
public function delete(Genre $genre)
{
$genre->delete();
$this->swalToast("The genre <b><i>{$genre->name}</i></b> has been deleted");
}
...
}
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
Advanced version
- With a few lines of code, we can replace the default JavaScript
confirm()
dialog with a more appealing dialog from the SweetAlert2 library
- Line 9 - 10: remove the
wire:confirm
attribute
php
<td>
@if($editGenre['id'] !== $genre->id)
<div
class="flex gap-1 justify-center [&>*]:cursor-pointer [&>*]:outline-0 [&>*]:transition">
<x-phosphor-pencil-line-duotone
wire:click="edit({{ $genre->id }})"
class="w-5 text-gray-300 hover:text-green-600"/>
<x-phosphor-trash-duotone
wire:click="delete({{ $genre->id }})"
{{-- wire:confirm="Are you sure you want to delete this genre?" --}}
class="w-5 text-gray-300 hover:text-red-600"/>
</div>
@endif
</td>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
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
Set the cursor in the input field
- Line 9: add
x-init="$el.focus()"
to input field$el
refers to the element itself (the input field)focus()
is a regular JavaScript function to sets the focus on the input field
php
@if($editGenre['id'] !== $genre->id)
<td
class="text-left cursor-pointer">{{ $genre->name }}
</td>
@else
<td>
<div class="flex flex-col text-left">
<x-input id="edit_{{ $genre->id }}" type="text"
x-init="$el.focus()"
wire:model="editGenre.name"
wire:keydown.enter="update({{ $genre->id }})"
wire:keydown.tab="update({{ $genre->id }})"
wire:keydown.escape="resetValues()"
class="w-48"/>
<x-input-error for="editGenre.name" class="mt-2"/>
</div>
</td>
@endif
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
Disable the input field
- Line 10 - 12: set the attribute
disabled
in the input field totrue
- Temporary add
sleep(2)
to theupdate()
method to simulate a slow server response
php
@if($editGenre['id'] !== $genre->id)
<td
class="text-left cursor-pointer">{{ $genre->name }}
</td>
@else
<td>
<div class="flex flex-col text-left">
<x-input id="edit_{{ $genre->id }}" type="text"
x-init="$el.focus()"
@keydown.enter="$el.setAttribute('disabled', true);"
@keydown.tab="$el.setAttribute('disabled', true);"
@keydown.esc="$el.setAttribute('disabled', true);"
wire:model="editGenre.name"
wire:keydown.enter="update({{ $genre->id }})"
wire:keydown.tab="update({{ $genre->id }})"
wire:keydown.escape="resetValues()"
class="w-48"/>
<x-input-error for="editGenre.name" class="mt-2"/>
</div>
</td>
@endif
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
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
update()
method or theresetValues()
method
(Add a 2-second delay to theupdate()
method to simulate a slow server response)
3: 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