# 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
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 to
records_old
and it's name torecords.old
- Update the navigation menu in resources/views/livewire/layout/nav-bar.blade.php
routes/web.php
resources/views/livewire/layout/nav-bar.blade.php
- Line 10: change the old get-route to
records_old
and it's name torecords.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
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
livewire/admin/records.blade.php
Livewire/Admin/Records.php
result
- 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 totrue
- Line 81: the modal is by default hidden and will be shown when the
<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
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
- It's important to get some basic knowledge about the Jetstream dialog modal (opens new window) before you continue
- You're probably going to use this modal in other projects as well, so it's good to know how it works
- Check this YouTube video for or more in-depth information about the Jetstream dialog modal:
- 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 ax-jet-modal
component
- This component contains tree slots (
- 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 Alpinesx-show
directive - This is where the connection between the modal and our
$showModal
property is made
- The most important part of this component is
- Ctrl + click on the
- Now we have two ways to hide the modal with the 'CANCEL' button in the footer of our modal
- 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 - 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
- Using Livewire:
- 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
2
3
4
5
6
7
8
9
10
11
12
# Read all records
# Show all the records in the table
Livewire/Admin/Records.php
livewire/admin/records.blade.php
result
- 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Add pagination to the table
livewire/admin/records.blade.php
Livewire/Admin/Records.php
result
- 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
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
Livewire/Admin/Records.php
livewire/admin/records.blade.php
result
- 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
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
- If
Livewire/Admin/Records.php (1)
Livewire/Admin/Records.php (2)
livewire/admin/records.blade.php
result
Step 1
- Because the
$noStock
filter is sometimes applied (whentrue
or1
) and sometimes not (whenfalse
or0
), 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
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
(iftrue
: filter, iffalse
: skip filter) - The problem is that we don't have a column for the cover in the database!
- Yes, we have a "generated" column
cover
, but this is only available AFTER the->get()
(or->paginate()
) method is called - So, we can't use this in our filter 😔
- Yes, we have a "generated" column
- Wath we can do, is make a scope for this in the Record model and use it in our query
app/Models/Record.php
Livewire/Admin/Records.php
livewire/admin/records.blade.php
result
- 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 themb_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 thepublic/storage/covers
folder
Depending on the state of the$exists
filter, themb_id
is added to the$covers
array - Line 25:
whereIn()
(opens new window) returns only the records where it'smb_id
value is available in the$covers
array
- Line 10: use the
- 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
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
- 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
)
# 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:
The Doors - L.A. Woman
2 Belgen - Trop Petit
Joy Division - Closer
Ramones - End of the Century
- mb_id:
e68f23df-61e3-4264-bfc3-17ac3a6f856b
- API: https://musicbrainz.org/ws/2/release/e68f23df-61e3-4264-bfc3-17ac3a6f856b?inc=artists&fmt=json (opens new window)
$response->title
= L.A. Woman$response->artist-credit[0]->artist->name
= The Doors$response->cover-art-archive->front
= true
# 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 therender()
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
2
3
4
5
6
7
8
9
10
11
12
13
14
- Now we can populate the modal
Livewire/Admin/Records.php
livewire/admin/records.blade.php (1)
livewire/admin/records.blade.php (2)
result
- 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Add the validation rules
Livewire/Admin/Records.php
livewire/admin/records.blade.php
result
- 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
andstock
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
- For now, we just validate the
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
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
Livewire/Admin/Records.php
result
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Get the necessary data from the MusicBrainz API
Livewire/Admin/Records.php
result
- 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
andcover
properties with the data from the API
- update the
- Line 18 - 23: the API call failed:
- reset the
artist
,title
andcover
properties to their original state - Line 22: create a custom error message for the
mb_id
field
- reset the
- Line 11 - 17: the API call was successful:
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
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
Livewire/Admin/Records.php
result
- 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 thepublic
disk->put('covers/' . $this->newRecord['mb_id'] . '.jpg', $originalCover)
will save the image to thecovers
folder with the namemb_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
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
Livewire/Admin/Records.php
result
- 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
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
, thestock
and theprice
fields - The
mb_id
, thetitle
and theartist
fields are read-only (updating themb_id
would mean that we create a new record)
# Enter edit mode
livewire/admin/records.blade.php
Livewire/Admin/Records.php
result
- Line 3: add a
wire:key
to thetr
element to make sure that each row has a unique key - Line 13: add a
wire:click
to the edit button to call thesetNewRecord()
method with the recordid
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
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 notnewRecord.id
is empty: we're in create modenewRecord.id
is not empty: we're in edit mode
livewire/admin/records.blade.php
result
- 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'])
isfalse
:- Line 4: change the title of the modal from New record to Edit record
- Line 8 and 20: remove the
mb_id
field and theGet 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 theid
of the record as parameter
- The Update record button must call the
<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
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
Livewire/Admin/Records.php
result
- 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
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
livewire/admin/records.blade.php
Livewire/Admin/Records.php
result 1
result 2
- 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
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
# 2: Delete the cover image from the server
- Update the
deleteRecord()
method so that the cover image is also deleted from the server
Create a new record
Delete the record
- Add a new record with a cover image (e.g mb_id =
c0afd87f-2f90-4c4d-b69d-ec150660fa5a
) - Open the cover in a new browser tab: http://vinyl_shop.test/storage/covers/c0afd87f-2f90-4c4d-b69d-ec150660fa5a.jpg (opens new window)
# 3: Jetstream confirmation modal
- Jetstream has actually two modal components:
x-confirmation-modal
andx-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
# 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