# Admin: records

  • We already created a admin/records page in the Eloquent models section
  • This page was not meant to be a real admin page, but was only to show how to use Eloquent models and was build with a regular Laravel view
  • In this chapter we will create a new records page with CRUD operations and this time we build it with Livewire

REMARK

Remember that you can always reset the database to its initial state by running the command:

php artisan migrate:fresh
Copied!
1

# Preparation

# Create a Records component

  • Create a new Livewire component with the terminal command php artisan make:livewire Admin/Records
    • app/Http/Livewire/Admin/Records.php (the component class)
    • resources/views/livewire/admin/records.blade.php (the component view)
  • Open the component class and add the vinylshop layout





 
 
 
 



class Records extends Component
{
    public function render()
    {
        return view('livewire.admin.records')
            ->layout('layouts.vinylshop', [
                'description' => 'Manage the records of your vinyl shop',
                'title' => 'Records',
            ]);
    }
}
Copied!
1
2
3
4
5
6
7
8
9
10
11

# Add a new route

  • Add a new get-route for the admin/records to the routes/web.php file and change the route to the old get-route torecords_old and it's name to records.old
  • Update the navigation menu in resources/views/livewire/layout/nav-bar.blade.php
  • Line 10: change the old get-route to records_old and it's name to records.old
  • Line 11: add a new route in the admin group
    • The URL is admin/records (prefix is already set to admin)
    • The component is the Records class
    • The route name is admin.records (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_old', [RecordController::class, 'index'])->name('records.old');
    Route::get('records', Records::class)->name('records');
});
...
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13

# Basic scaffolding

  • Let's start with a basic scaffolding for the view and the component
  • After the previous chapter, we already have a good idea of which properties and methods we'll need in the component to create a CRUD page
  • Additional properties and methods can be added later
  • Open the resources/views/livewire/admin/records.blade.php and replace the content with the code beneath
  • On top of the page, we have a x-tmk.section for the filters and a button to create a new record
  • Below the filters, we have a second x-tmk.section for the table with the records
  • At the bottom of the page, stands a x-jet-dialog-modal for the create and update functions
    • Line 81: the modal is by default hidden and will be shown when the showModal property in the component class is set to true
















































































 












<div>
    <x-tmk.section class="mb-4 flex gap-2">
        <div class="flex-1">
            <x-jet-input id="search" type="text" placeholder="Filter Artist Or Record"
                         class="w-full shadow-md placeholder-gray-300"/>
        </div>
        <x-tmk.form.switch id="noStock"
                           text-off="No stock"
                           color-off="bg-gray-100 before:line-through"
                           text-on="No stock"
                           color-on="text-white bg-lime-600"
                           class="w-20 h-11"/>
        <x-tmk.form.switch id="noCover"
                           text-off="Records without cover"
                           color-off="bg-gray-100 before:line-through"
                           text-on="Records without cover"
                           color-on="text-white bg-lime-600"
                           class="w-44 h-11"/>
        <x-jet-button>
            new record
        </x-jet-button>
    </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-20">
                <col class="w-14">
                <col class="w-max">
                <col class="w-24">
            </colgroup>
            <thead>
            <tr class="bg-gray-100 text-gray-700 [&>th]:p-2">
                <th>#</th>
                <th></th>
                <th>Price</th>
                <th>Stock</th>
                <th class="text-left">Record</th>
                <th>
                    <x-tmk.form.select id="perPage"
                                       class="block mt-1 w-full">
                        <option value="5">5</option>
                        <option value="10">10</option>
                        <option value="15">15</option>
                        <option value="20">20</option>
                    </x-tmk.form.select>
                </th>
            </tr>
            </thead>
            <tbody>
            <tr class="border-t border-gray-300">
                <td>...</td>
                <td>
                    <img src="/storage/covers/no-cover.png" 
                         alt="no cover"
                         class="my-2 border object-cover">
                </td>
                <td>...</td>
                <td>...</td>
                <td class="text-left">...</td>
                <td>
                    <div class="border border-gray-300 rounded-md overflow-hidden m-2 grid grid-cols-2 h-10">
                        <button
                            class="text-gray-400 hover:text-sky-100 hover:bg-sky-500 transition border-r border-gray-300">
                            <x-phosphor-pencil-line-duotone class="inline-block w-5 h-5"/>
                        </button>
                        <button
                            class="text-gray-400 hover:text-red-100 hover:bg-red-500 transition">
                            <x-phosphor-trash-duotone class="inline-block w-5 h-5"/>
                        </button>
                    </div>
                </td>
            </tr>
            </tbody>
        </table>
    </x-tmk.section>

    <x-jet-dialog-modal  id="recordModal"
        wire:model="showModal">
        <x-slot name="title">
            <h2>title</h2>
        </x-slot>
        <x-slot name="content">
            content
        </x-slot>
        <x-slot name="footer">
            <x-jet-secondary-button>Cancel</x-jet-secondary-button>
        </x-slot>
    </x-jet-dialog-modal>
</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
90
91
92

# Jetstream dialog modal

REMARKS

  • The Jetstream dialog modal (opens new window) is a component that is used to show/hide a modal with a title, content and footer
  • More specifically, it's a component that's build on top of a second component
    • Ctrl + click on the x-jet-dialog-modal tag to see the code of the first component
      • This component contains tree slots ($title, $content and $footer) that are wrapped in inside a x-jet-modal component
    • Ctrl + click on the x-jet-modal tag to see the code of the second component
      • The most important part of this component is show: @entangle($attributes->wire('model')).defer and Alpines x-show directive
      • This is where the connection between the modal and our $showModal property is made
  • Now we have two ways to hide the modal with the 'CANCEL' button in the footer of our modal
    1. Using Livewire: <x-jet-secondary-button wire:click="$set('showModal', false)">Cancel</x-jet-secondary-button>
      This is the slower way because it will first update the $showModal property on the backend and then the modal will be hidden on the frontend
    2. Using Alpine: <x-jet-secondary-button @click="show = false">Cancel</x-jet-secondary-button>
      This is the faster way because it will first hide the modal in the frontend, and then it will update the $showModal property on the backend
  • Add the @click="show = false" to the 'CANCEL' button in the footer of the modal and check the result









 



<x-jet-dialog-modal id="recordModal"
    wire:model="showModal">
    <x-slot name="title">
        <h2>title</h2>
    </x-slot>
    <x-slot name="content">
        content
    </x-slot>
    <x-slot name="footer">
        <x-jet-secondary-button @click="show = false">Cancel</x-jet-secondary-button>
    </x-slot>
</x-jet-dialog-modal>
Copied!
1
2
3
4
5
6
7
8
9
10
11
12

# Read all records

# Show all the records in the table

  • Line 3: add the WithPagination trait to the class
  • Line 13: get all the records from the database and order them by artist and title
  • Line 14: paginate the records
  • Line 15: send the records to the view


 









 
 
 






class Records extends Component
{
    use WithPagination;

    ...
    
    public $perPage = 5;
    
    ...

    public function render()
    {
        $records = Record::orderBy('artist')->orderBy('title')
            ->paginate($this->perPage);
        return view('livewire.admin.records', compact('records'))
            ->layout('layouts.vinylshop', [
                'description' => 'Manage the records of your vinyl shop',
                'title' => 'Records',
            ]);
    }
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# Add pagination to the table

  • Line 14: bind the select element to the $perPage property
  • Line 2 and 26 add the pagination links before and after the table

 











 











 


<x-tmk.section>
  <div class="my-4">{{ $records->links() }}</div>
  <table class="text-center w-full border border-gray-300">
      <colgroup> ... </colgroup>
      <thead>
      <tr class="bg-gray-100 text-gray-700 [&>th]:p-2">
          <th>#</th>
          <th></th>
          <th>Price</th>
          <th>Stock</th>
          <th class="text-left">Record</th>
          <th>
              <x-tmk.form.select id="perPage"
                                 wire:model="perPage"
                                 class="block mt-1 w-full">
                  <option value="5">5</option>
                  <option value="10">10</option>
                  <option value="15">15</option>
                  <option value="20">20</option>
              </x-tmk.form.select>
          </th>
      </tr>
      </thead>
      <tbody> ... </tbody>
  </table>
  <div class="my-4">{{ $records->links() }}</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
22
23
24
25
26
27

# Filter by artist or title

  • Line 8: add the scope searchByArtistOrTitle(), that we made earlier in this course, to the query







 










class Records extends Component
{
    ...
    
    public function render()
    {
        $records = Record::orderBy('artist')->orderBy('title')
            ->searchTitleOrArtist($this->search)
            ->paginate($this->perPage);
        return view('livewire.admin.records', compact('records'))
            ->layout('layouts.vinylshop', [
                'description' => 'Manage the records of your vinyl shop',
                'title' => 'Records',
            ]);
    }
    ...
}
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Filter by records in stock

  • The <x-tmk.form.switch id="noStock" ... /> is just a wrapper around a checkbox
  • The state of this checkbox determines whether we should filter or not
    • If checked, filter the query further by ->where('stock', '=', 0)
    • If unchecked, skip this filter

Step 1

  • Because the $noStock filter is sometimes applied (whentrue or 1) and sometimes not (when falseor 0), we need to split the query in two parts
  • The result of the total query is exactly the same as before







 
 

 








class Records extends Component
{
    ...
    
    public function render()
    {
        // filter by $search
        $query = Record::orderBy('artist')->orderBy('title')
            ->searchTitleOrArtist($this->search);
        // paginate the $query
        $records = $query->paginate($this->perPage);
        return view('livewire.admin.records', compact('records'))
            ->layout('layouts.vinylshop', [
                'description' => 'Manage the records of your vinyl shop',
                'title' => 'Records',
            ]);
    }
}
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# Filter by records without cover image

  • The <x-tmk.form.switch id="noCover" ... /> acts just the same as the switch for $noStock
    (if true: filter, if false: skip filter)
  • The problem is that we don't have a column for the cover in the database!
    • Yes, we have a "generated" columncover, but this is only available AFTER the ->get() (or ->paginate()) method is called
    • So, we can't use this in our filter 😔
  • Wath we can do, is make a scope for this in the Record model and use it in our query
  • Open the model app/Models/Record.php
  • Add a new scope that returns only records when there is no cover available in the public/storage/covers folder
    • Line 10: use the pluck() method (opens new window) to fill the $mb_ids array with the mb_id values
      IMPORTANT: ONLY the result from the previous query is used in this scope!
      (e.g. im_dbs = ['fcb78d0d-8067-4b93-ae58-1e4347e20216', 'd883e644-5ec0-4928-9ccd-fc78bc306a46', ...])
    • Line 12: initialize an empty array $covers
    • Line 13 - 23: loop through the $mb_ids array and check if the cover exists in the public/storage/covers folder
      Depending on the state of the $exists filter, the mb_id is added to the $covers array
    • Line 25: whereIn() (opens new window) returns only the records where it's mb_id value is available in the $covers array
  • IMPORTANT:
    • open the menu Laravel > Generate Helper Code to add the new scope for type hinting and auto-completion in PhpStorm






 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

...

public function scopeMaxPrice($query, $price) { ... }

public function scopeSearchTitleOrArtist($query, $search = '%') { ...}

public function scopeCoverExists($query, $exists = true)
{
    // make an array with all the mb_id attributes
    $mb_ids = $query->pluck('mb_id');
    // empty array to store 'mb_id's that have a cover
    $covers = [];
    foreach ($mb_ids as $mb_id) {
        // $exists = true: if the cover exists, add the mb_id to the $covers array
        // $exists = false: if the cover does not exist, add the mb_id to the $covers array
        if ($exists) {
            if (Storage::disk('public')->exists('covers/' . $mb_id . '.jpg'))
                $covers[] = $mb_id;
        } else {
            if (!Storage::disk('public')->exists('covers/' . $mb_id . '.jpg'))
                $covers[] = $mb_id;
        }
    }
    // return only the records with the mb_id in the $covers array
    return $query->whereIn('mb_id', $covers);
}
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

# Create a new record

# Find the MusicBrainz ID (mb_id) of the record

  • Go to https://musicbrainz.org/ (opens new window)
  • Search, in the right top corner, an artist (e.g. The Doors)
  • Click on the artist name
    Search artist
  • Now click on one of the albums (e.g. L.A. Woman)
  • You get a list of all the releases of this album. Click on one of them (be sure to choose for a release on vinyl!).
  • The property mb_id in our records table is the code at the end of the URL (e.g. e68f23df-61e3-4264-bfc3-17ac3a6f856b)
    Search record

# Get the data from MusicBrainz API

  • Now that we have the mb_id of the record, we can use the MusicBrainz API (opens new window) to extract the data from the record
  • We need: the title of the record, the artist and the cover (if there is one)
    These fields are not editable, except for the cover later in this course
  • We also need the price of the record, how many items are in stock and the genre this record belongs to
    These fields are editable and don't have anything to do with the MusicBrainz API
  • Some examples on how the extract the data that we need from the JSON response:

# Add form fields to the modal

  • Our modal needs to have the following fields:
    • mb_id (text input)
    • title and artist (hidden inputs because they are not editable)
    • genre (select input with all the genres)
    • price and stock (number inputs)
  • Get all the genres
    • We need to get all the genres from the database and put them in a select input
    • Because this list don't change while we are on this page, we can get it once (use the mount() methode for this, not the render() methode) and store it in a new $genres property


 



 
 
 
 
 




class Records extends Component
{
      public $genres;
      
      ...
      
      // get all the genres from the database (runs only once)
      public function mount()
      {
        $this->genres = Genre::orderBy('name')->get();
      }
      
      public function render() { ... }
}
Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • Now we can populate the modal
  • Update the setNewRecord() methode







 
 
 






class Records extends Component
{
    ...
    
    // set/reset $newRecord and validation
    public function setNewRecord()
    {
        $this->resetErrorBag();
        $this->reset('newRecord');
        $this->showModal = true;
    }
    
    ...
}

Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# Add the validation rules

  • Line 9 - 14: add the validation rules to the rules() method
    • All fields are required
    • The mb_id field be exactly 36 characters long and must be unique in the database
    • The genre_id field must be a valid genre id (opens new window)
    • The price and stock fields must be a number and can't be negative
  • Line 19 - 21: add a new getDataFromMusicbrainzApi() method to fetch the Musicbrainz API
    • For now, we just validate the $newRecord property to test our validation rules








 
 
 
 
 
 



 
 
 
 





class Records extends Component
{
    ...
    
    // validation rules (use the rules() method, not the $rules property)
    protected function rules()
    {
        return [
            'newRecord.mb_id' => 'required|size:36|unique:records,mb_id,' . $this->newRecord['id'],
            'newRecord.artist' => 'required',
            'newRecord.title' => 'required',
            'newRecord.genre_id' => 'required|exists:genres,id',
            'newRecord.stock' => 'required|numeric|min:0',
            'newRecord.price' => 'required|numeric|min:0',
        ];
    }
    
    // get artist, title and cover from the MusicBrainz API
    public function getDataFromMusicbrainzApi() {
        $this->validate();
    }
    
    ...
}

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

# Update the validation messages







 
 
 
 
 
 






class Records extends Component
{
    ...
    
    // validation attributes
    protected $validationAttributes = [
        'newRecord.mb_id' => 'MusicBrainz id',
        'newRecord.artist' => 'artist name',
        'newRecord.title' => 'record title',
        'newRecord.genre_id' => 'genre',
        'newRecord.stock' => 'stock',
        'newRecord.price' => 'price',
    ];
    
    ...
}

Copied!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Get the necessary data from the MusicBrainz API

  • Line 8: replace $this->validate() with $this->validateOnly('newRecord.mb_id')
  • Line 9: reset the error bag
  • Line 10: try to fetch the data from the MusicBrainz API
    • Line 11 - 17: the API call was successful:
      • update the artist, title and cover properties with the data from the API
    • Line 18 - 23: the API call failed:
      • reset the artist, title and cover properties to their original state
      • Line 22: create a custom error message for the mb_id field







 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 






class Records extends Component
{
    ...
    
    // get artist, title and cover from the MusicBrainz API
    public function getDataFromMusicbrainzApi()
    {
        $this->validateOnly('newRecord.mb_id');
        $this->resetErrorBag();
        $response = Http::get('https://musicbrainz.org/ws/2/release/' . $this->newRecord['mb_id'] . '?inc=artists&fmt=json');
        if ($response->successful()) {
            $data = $response->json();
            $this->newRecord['artist'] = $data['artist-credit'][0]['artist']['name'];
            $this->newRecord['title'] = $data['title'];
            if ($data['cover-art-archive']['front']) {
                $this->newRecord['cover'] = 'https://coverartarchive.org/release/' . $this->newRecord['mb_id'] . '/front-250.jpg';
            }
        } else {
            $this->newRecord['artist'] = null;
            $this->newRecord['title'] = null;
            $this->newRecord['cover'] = '/storage/covers/no-cover.png';
            $this->addError('newRecord.mb_id', 'MusicBrainz id not found');
        }
    }
    
    ...
}

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

# Save the cover image to the server

  • With only two extra lines of code, we can save the cover image to the server
  • Line 17 we use the Intervention Image (opens new window) package to read and compress the original image
    • $originalCover = Image::make($this->newRecord['cover']) will create a new image instance from the URL
    • ->encode('jpg', 75) will encode the image to a JPG file with a quality of 75%
  • Line 18: use Laravel's File Storage (opens new window) to save the image to the server
    • Storage::disk('public') will save the image to the public disk
    • ->put('covers/' . $this->newRecord['mb_id'] . '.jpg', $originalCover) will save the image to the covers folder with the name mb_id value and the extension .jpg
















 
 








class Records extends Component
{
    ...
    
    // get artist, title and cover from the MusicBrainz API
    public function getDataFromMusicbrainzApi()
    {
        $this->validateOnly('newRecord.mb_id');
        $this->resetErrorBag();
        $response = Http::get('https://musicbrainz.org/ws/2/release/' . $this->newRecord['mb_id'] . '?inc=artists&fmt=json');
        if ($response->successful()) {
            $data = $response->json();
            $this->newRecord['artist'] = $data['artist-credit'][0]['artist']['name'];
            $this->newRecord['title'] = $data['title'];
            if ($data['cover-art-archive']['front']) {
                $this->newRecord['cover'] = 'https://coverartarchive.org/release/' . $this->newRecord['mb_id'] . '/front-250.jpg';
                $originalCover = Image::make($this->newRecord['cover'])->encode('jpg', 75);
                Storage::disk('public')->put('covers/' . $this->newRecord['mb_id'] . '.jpg', $originalCover);
            }
        } else { ... }
    }
    
    ...
}

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

# Save the record to the database

  • Now that we have all the data we need, we can save the record to the database
  • Line 8: validate if all the required fields are filled in
  • Line 9 - 16: create a new record with the data from the $newRecord properties
  • Line 17: hide the modal
  • Line 18 - 21: show a success toast message







 
 
 
 
 
 
 
 
 
 
 
 
 
 





class Records extends Component
{
  

    // create a new record
    public function createRecord()
    {
        $this->validate();
        $record = Record::create([
            'mb_id' => $this->newRecord['mb_id'],
            'artist' => $this->newRecord['artist'],
            'title' => $this->newRecord['title'],
            'stock' => $this->newRecord['stock'],
            'price' => $this->newRecord['price'],
            'genre_id' => $this->newRecord['genre_id'],
        ]);
        $this->showModal = false;
        $this->dispatchBrowserEvent('swal:toast', [
            'background' => 'success',
            'html' => "The record <b><i>{$record->title} from {$record->artist}</i></b> has been added",
        ]);
    }
    
    ...
}
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

# Update a record

  • All we have to do to edit a record is to set the values of the $newRecord array to the values of the record we want to edit
  • This can be done by passing the record id to the setNewRecord() method, and then we can also re-use the modal to update the record
  • We're only allowed to update the genre_id, the stock and the price fields
  • The mb_id, the title and the artist fields are read-only (updating the mb_id would mean that we create a new record)

# Enter edit mode

  • Line 3: add a wire:key to the tr element to make sure that each row has a unique key
  • Line 13: add a wire:click to the edit button to call the setNewRecord() method with the record id as parameter


 









 














@forelse($records as $record)
    <tr
        wire:key="record_{{ $record->id }}"
        class="border-t border-gray-300">
        <td>{{ $record->id }}</td>
        <td> ... </td>
        <td>{{ $record->price_euro }}</td>
        <td>{{ $record->stock }}</td>
        <td class="text-left"> ... </td>
        <td>
            <div class="border border-gray-300 rounded-md overflow-hidden m-2 grid grid-cols-2 h-10">
                <button
                    wire:click="setNewRecord({{ $record->id }})"
                    class="text-gray-400 hover:text-sky-100 hover:bg-sky-500 transition border-r border-gray-300">
                    <x-phosphor-pencil-line-duotone class="inline-block w-5 h-5"/>
                </button>
                <button
                    class="text-gray-400 hover:text-red-100 hover:bg-red-500 transition">
                    <x-phosphor-trash-duotone class="inline-block w-5 h-5"/>
                </button>
            </div>
        </td>
    </tr>
@empty
    ...
@endforelse
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

# Refactor the modal

  • We can use e.g. the value of the newRecord.id property to determine if we're in edit mode or not
    • newRecord.id is empty: we're in create mode
    • newRecord.id is not empty: we're in edit mode
  • Use the PHP is_null($newRecord['id']) function to determine if we're in edit mode or not
  • In edit mode is_null($newRecord['id']) is false:
    • Line 4: change the title of the modal from New record to Edit record
    • Line 8 and 20: remove the mb_id field and the Get Record info button in edit mode
    • Line 25, 31 - 38: remove the Save new record button and add a Update record button in edit mode
      • The Update record button must call the updateRecord() method when clicked and pass the id of the record as parameter



 



 











 




 





 
 
 
 
 
 
 
 



<x-jet-dialog-modal id="recordModal"
                        wire:model="showModal">
    <x-slot name="title">
        <h2>{{ is_null($newRecord['id']) ? 'New record' : 'Edit record' }}</h2>
    </x-slot>
    <x-slot name="content">
        @if ($errors->any()) ... @endif
        @if(is_null($newRecord['id']))
            <x-jet-label for="mb_id" value="MusicBrainz id"/>
            <div class="flex flex-row gap-2 mt-2">
                <x-jet-input id="mb_id" type="text" placeholder="MusicBrainz ID"
                             wire:model.defer="newRecord.mb_id"
                             class="flex-1"/>
                <x-jet-button
                    wire:click="getDataFromMusicbrainzApi()"
                    wire:loading.attr="disabled">
                    Get Record info
                </x-jet-button>
            </div>
        @endif
        <div class="flex flex-row gap-4 mt-4"> ... </div>
    </x-slot>
    <x-slot name="footer">
        <x-jet-secondary-button @click="show = false">Cancel</x-jet-secondary-button>
        @if(is_null($newRecord['id']))
            <x-jet-button
                wire:click="createRecord()"
                wire:loading.attr="disabled"
                class="ml-2">Save new record
            </x-jet-button>
        @else
            <x-jet-button
                color="success"
                wire:click="updateRecord({{ $newRecord['id'] }})"
                wire:loading.attr="disabled"
                class="ml-2">Update record
            </x-jet-button>
        @endif
    </x-slot>
</x-jet-dialog-modal>
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

# Update the record

  • Line 6: use route model binding to get the record to update
  • Line 8: validate if all the required fields are filled in
  • Line 9 - 16: update the record with the data from the $newRecord properties
  • Line 17: hide the modal
  • Line 18 - 21 show a success toast message





 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 




class Records extends Component
{
    ...
    
    // update an existing record
    public function updateRecord(Record $record)
    {
        $this->validate();
        $record->update([
            'mb_id' => $this->newRecord['mb_id'],
            'artist' => $this->newRecord['artist'],
            'title' => $this->newRecord['title'],
            'stock' => $this->newRecord['stock'],
            'price' => $this->newRecord['price'],
            'genre_id' => $this->newRecord['genre_id'],
        ]);
        $this->showModal = false;
        $this->dispatchBrowserEvent('swal:toast', [
            'background' => 'success',
            'html' => "The record <b><i>{$record->title} from {$record->artist}</i></b> has been updated",
        ]);
    }
    
    ...
}
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 record

  • Line 18: convert the button to an Alpine component
  • Line 19: use the vanilla JavaScript confirm() function to ask the user if he really wants to delete the record

















 
 










@forelse($records as $record)
    <tr
        wire:key="record_{{ $record->id }}"
        class="border-t border-gray-300">
        <td>{{ $record->id }}</td>
        <td> ... </td>
        <td>{{ $record->price_euro }}</td>
        <td>{{ $record->stock }}</td>
        <td class="text-left"> ... </td>
        <td>
            <div class="border border-gray-300 rounded-md overflow-hidden m-2 grid grid-cols-2 h-10">
                <button
                    wire:click="setNewRecord({{ $record->id }})"
                    class="text-gray-400 hover:text-sky-100 hover:bg-sky-500 transition border-r border-gray-300">
                    <x-phosphor-pencil-line-duotone class="inline-block w-5 h-5"/>
                </button>
                <button
                    x-data=""
                    @click="confirm('Are you sure you want to delete this record?') ? $wire.deleteRecord({{ $record->id }}) : ''"
                    class="text-gray-400 hover:text-red-100 hover:bg-red-500 transition">
                    <x-phosphor-trash-duotone class="inline-block w-5 h-5"/>
                </button>
            </div>
        </td>
    </tr>
@empty
    ...
@endforelse
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

# EXERCISES:

# 1: Background color

  • Give all the records that are out of stock a red background color out of stock

# 2: Delete the cover image from the server

  • Update the deleteRecord() method so that the cover image is also deleted from the server

# 3: Jetstream confirmation modal

  • Jetstream has actually two modal components: x-confirmation-modal and x-dialog-modal
    (see resources/views/vendor/jetstream/components/)
  • Examine the code for the confirmation modal and try to use them to confirm the deletion of a record
  • TIPS:
    • add a new property to toggle the modal
    • add a new method to update some values in the $newRecord property, so they can be used in the modal
      Delete confirmation modal

# 3: Clear the search field

  • Placed a little X over the input field to clear the search field
  • The X is only visible when search field in not empty
  • Tip: see Shop component for an example Clear search field
Last Updated: 12/14/2022, 6:12:16 AM