# 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
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',
            ]);
    }
}
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

# Basic scaffolding for view

  • Open resources/views/livewire/admin/genres.blade.php and replace the content with the following code:

# 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

# Order the table by clicking on the column headers

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

# Create a new genre

# Add a new genre

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();
      
      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

# 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

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

# 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

# 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

# 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');
        ...
    }
    
    ...
}
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
];
1
2
3
  • to:
public function rules()
{
    return [
        // validation rules here
    ];
}
1
2
3
4
5
6

# 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

# 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

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

# 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>
1
2
3
4
5
6
7
8
9
10
11
12
13

# Disable the input field

# Use SweetAlert2 for the confirmation dialog

# Basic version

  • Replace the vanilla JavaScript confirm() dialog box with a SweetAlert2 dialog

# 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

# 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