Appearance
Shop: master section
REMARKS
- Because this is our first full-blown Livewire component in this course, we will split the solution in 3 parts:
- Part 1: let's start with the "master page" where all the records are shown on a card and add some pagination
- Part 2: when the user clicks on a record, a modal pops up with some extra details (the tracks) of the record
- Part 3: finally, we'll add some filtering (by name, by price and by genre) and sorting
- How to actually order a record in the shop is a bit advanced and will be explained in a later chapter
Preparation
Create the Shop component
- Make a new Livewire component with the terminal command
php artisan livewire:make Shop
- This command creates two files:
- app/Livewire/Shop.php: the component class contains the logic (= the controller)
- resources/views/livewire/shop.blade.php: the view
- This is a basic skeleton of a Livewire component with only the
render()
method- The
render()
method is one of the so-called Lifecycle Hooks that will be executed every time the component is rendered for the first time AND when something in the component is updated (e.g. a public property has changed) - For now, it only returns the resources/views/livewire/shop.blade.php view
- The
php
namespace App\Livewire;
use Livewire\Component;
class Shop extends Component
{
public function render()
{
return view('livewire.shop');
}
}
namespace App\Livewire;
use Livewire\Component;
class Shop extends Component
{
public function render()
{
return view('livewire.shop');
}
}
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 get-route to the routes/web.php file
- Update the navigation menu in resources/views/components/layout/nav.blade.php
- Add a get-route to the routes/web.php file
- The shop route, with the name shop will be handled by the Shop::class component
- Don't forget to import the Shop component class at the top of the file (
use App\Livewire\Shop;
)
php
Route::view('/', 'home')->name('home');
Route::get('shop', Shop::class)->name('shop');
Route::view('contact', 'contact')->name('contact');
Route::view('playground', 'playground')->name('playground');
Route::view('under-construction', 'under-construction')->name('under-construction');
...
Route::view('/', 'home')->name('home');
Route::get('shop', Shop::class)->name('shop');
Route::view('contact', 'contact')->name('contact');
Route::view('playground', 'playground')->name('playground');
Route::view('under-construction', 'under-construction')->name('under-construction');
...
1
2
3
4
5
6
7
2
3
4
5
6
7
Reference the vinylshop layout template
- To fix the previous error, we must add a reference to the appropriate template (layouts/vinylshop.blade.php) to the view
- Line 10: add the
Layout
attribute with reference to the layouts/vinylshop.blade.php template and pass the pagetitle
anddescription
as parameters - Line 5: import the
Layout
attribute withuse Livewire\Attributes\Layout;
(orAlt + Enter
on the attribute name and choose Import class
php
<?php
namespace App\Livewire;
use Livewire\Attributes\Layout;
use Livewire\Component;
class Shop extends Component
{
#[Layout('layouts.vinylshop', ['title' => 'Shop', 'description' => 'Welcome to our shop'])]
public function render()
{
return view('livewire.shop');
}
}
<?php
namespace App\Livewire;
use Livewire\Attributes\Layout;
use Livewire\Component;
class Shop extends Component
{
#[Layout('layouts.vinylshop', ['title' => 'Shop', 'description' => 'Welcome to our shop'])]
public function render()
{
return view('livewire.shop');
}
}
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
Get all records
- It's time to get all the records and render a card for each one of them
- Getting the records is achieved INSIDE the
render()
method in the component, and you pass them, with thecompact()
methode, to the view - Loop through the records in the view and display them all
IMPORTANT NOTICE ABOUT LIVEWIRE LOOPS
- ALWAYS use the
wire:key
directive inside a loop to make sure that Livewire can keep track of the records- This is necessary because the records are displayed in a loop
- If you don't use the
wire:key
directive, Livewire will not be able to keep track of the records and will not be able to update them - The
wire:key
directive is a unique identifier for each record - In this case, we could use the
id
or themb_id
attribute of the record
php
class Shop extends Component
{
#[Layout('layouts.vinylshop', ['title' => 'Shop', 'description' => 'Welcome to our shop'])]
public function render()
{
$records = Record::orderBy('artist')
->get();
return view('livewire.shop', compact('records'));
}
}
class Shop extends Component
{
#[Layout('layouts.vinylshop', ['title' => 'Shop', 'description' => 'Welcome to our shop'])]
public function render()
{
$records = Record::orderBy('artist')
->get();
return view('livewire.shop', compact('records'));
}
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Update the card
TIP
- If you can't remember the attributes of a model, you can (temporary) add the
x-tmk.livewire-log
component just before the closingdiv
tag in the view
php
<div>
...
<x-tmk.livewire-log :records="$records" />
</div>
<div>
...
<x-tmk.livewire-log :records="$records" />
</div>
1
2
3
4
5
2
3
4
5
Add the correct information to the card
- Update the card within the
foreach
loop with the correct information about the record
- Line 6 - 8: replace the
src
attribute with the correct image, and update thealt
andtitle
attributes - Line 11: replace the
Artist
text with$record->artist
- Line 12: replace the
Title
text with$record->title
- Line 13: replace the
Genre
text with$record->genre_name
- Line 16: replace the
Price
text with$record->price_euro
php
@foreach ($records as $record)
<div
wire:key="record-{{ $record->id }}"
class="flex bg-white border border-gray-300 shadow-md rounded-lg overflow-hidden">
<img class="w-52 h-52 border-r border-gray-300 object-cover"
src="{{ $record->cover }}"
alt="{{ $record->title }}"
title="{{ $record->title }}">
<div class="flex-1 flex flex-col">
<div class="flex-1 p-4">
<p class="text-lg font-medium">{{ $record->artist }}</p>
<p class="italic pb-2">{{ $record->title }}</p>
<p class="italic font-thin text-right pt-2 mb-2">{{ $record->genre_name }}</p>
</div>
<div class="flex justify-between border-t border-gray-300 bg-gray-100 px-4 py-2">
<div>{{ $record->price_euro }}</div>
<div class="flex space-x-4">
<button class="w-6 hover:text-red-900">
<x-phosphor-shopping-bag-light/>
</button>
<button class="w-6 hover:text-red-900">
<x-phosphor-music-notes-light/>
</button>
</div>
</div>
</div>
</div>
@endforeach
@foreach ($records as $record)
<div
wire:key="record-{{ $record->id }}"
class="flex bg-white border border-gray-300 shadow-md rounded-lg overflow-hidden">
<img class="w-52 h-52 border-r border-gray-300 object-cover"
src="{{ $record->cover }}"
alt="{{ $record->title }}"
title="{{ $record->title }}">
<div class="flex-1 flex flex-col">
<div class="flex-1 p-4">
<p class="text-lg font-medium">{{ $record->artist }}</p>
<p class="italic pb-2">{{ $record->title }}</p>
<p class="italic font-thin text-right pt-2 mb-2">{{ $record->genre_name }}</p>
</div>
<div class="flex justify-between border-t border-gray-300 bg-gray-100 px-4 py-2">
<div>{{ $record->price_euro }}</div>
<div class="flex space-x-4">
<button class="w-6 hover:text-red-900">
<x-phosphor-shopping-bag-light/>
</button>
<button class="w-6 hover:text-red-900">
<x-phosphor-music-notes-light/>
</button>
</div>
</div>
</div>
</div>
@endforeach
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
Hide the order button when not in stock
- Showing the order button is only relevant when the record is in stock
- If the record is not in stock, we should hide it and replace it with a message SOLD OUT
- Line 4: check if the record is in stock or not:
- in stock: show the order button
- not in stock: hide the order button and show the message SOLD OUT
php
<div class="flex justify-between border-t border-gray-300 bg-gray-100 px-4 py-2">
<div>{{ $record->price_euro }}</div>
<div class="flex space-x-4">
@if($record->stock > 0)
<button class="w-6 hover:text-red-900">
<x-phosphor-shopping-bag-light/>
</button>
@else
<p class="font-extrabold text-red-700">SOLD OUT</p>
@endif
<button class="w-6 hover:text-red-900">
<x-phosphor-music-notes-light/>
</button>
</div>
</div>
<div class="flex justify-between border-t border-gray-300 bg-gray-100 px-4 py-2">
<div>{{ $record->price_euro }}</div>
<div class="flex space-x-4">
@if($record->stock > 0)
<button class="w-6 hover:text-red-900">
<x-phosphor-shopping-bag-light/>
</button>
@else
<p class="font-extrabold text-red-700">SOLD OUT</p>
@endif
<button class="w-6 hover:text-red-900">
<x-phosphor-music-notes-light/>
</button>
</div>
</div>
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
Pagination
- Showing all the records on one page is not a good idea, so we will add pagination to the page
- We add a public property
$perPage
to the component to keep track of the number of records per page
(We'll change this value later in the view to make it more dynamic)
Add pagination to the component
- Line 15: add the public property
$perPage
with a default value of e.g.6
to the component - Pagination:
- Line 21: replace the
get()
method withpaginate($this->perPage)
- Line 12: add the
use WithPagination
trail to ensure the view can use the pagination method - Line 8: add the
use Livewire\WithPagination;
statement at the top of the file
- Line 21: replace the
php
<?php
namespace App\Livewire;
use App\Models\Record;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;
class Shop extends Component
{
use WithPagination;
// public properties
public $perPage = 6;
#[Layout('layouts.vinylshop', ['title' => 'Shop', 'description' => 'Welcome to our shop'])]
public function render()
{
$records = Record::orderBy('artist')
->paginate($this->perPage);
return view('livewire.shop', compact('records'));
}
}
<?php
namespace App\Livewire;
use App\Models\Record;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;
class Shop extends Component
{
use WithPagination;
// public properties
public $perPage = 6;
#[Layout('layouts.vinylshop', ['title' => 'Shop', 'description' => 'Welcome to our shop'])]
public function render()
{
$records = Record::orderBy('artist')
->paginate($this->perPage);
return view('livewire.shop', compact('records'));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Custom pagination links
- The default pagination links are not how we want them to be, so we will change the default layout and highlight the current page with a darker background color
- Before we can do this we need to pull Livewires pagination component out of the framework and into our project
- This is done by running the following command in the terminal:
php artisan livewire:publish --pagination
- This will copy the 4 pagination views to the
resources/views/vendor/livewire
folder- You can safely delete three of them because we only need the
tailwind.blade.php
view
- You can safely delete three of them because we only need the
- This is done by running the following command in the terminal:
- Open the resources/views/vendor/livewire/tailwind.blade.php view, find the classes for the current page and change two classes (line 91):
- from:
... text-gray-500 bg-white ...
- to:
... text-white bg-gray-800 ...
- from:
php
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
<span wire:key="...">
@if ($page == $paginator->currentPage())
<span aria-current="page">
<span class="relative inline-flex ... text-white bg-gray-800 ...">{{ $page }}</span>
</span>
@else
<button ...>
{{ $page }}
</button>
@endif
</span>
@endforeach
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
<span wire:key="...">
@if ($page == $paginator->currentPage())
<span aria-current="page">
<span class="relative inline-flex ... text-white bg-gray-800 ...">{{ $page }}</span>
</span>
@else
<button ...>
{{ $page }}
</button>
@endif
</span>
@endforeach
@endif
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
Add a preloader
- Switching between pages is very fast (every click is a round-trip to the server), but it is still nice to show a preloader while the page is updating in the background
- Add a public property
$loading
with a default value of'Loading records...'
php
// public properties
public $perPage = 6;
public $loading = 'Please wait...';
// public properties
public $perPage = 6;
public $loading = 'Please wait...';
1
2
3
2
3
TIP
- If you don't see the preloader because the page is loading too fast, you can add a temporary
sleep(2)
statement in therender()
method of the component - Don't forget to remove the
sleep()
statement when you are done testing
php
public function render()
{
sleep(2);
$records = Record::orderBy('artist')
->paginate($this->perPage);
return view('livewire.shop', compact('records'));
}
public function render()
{
sleep(2);
$records = Record::orderBy('artist')
->paginate($this->perPage);
return view('livewire.shop', compact('records'));
}
1
2
3
4
5
6
7
2
3
4
5
6
7
Add a tooltip to the buttons
- We use the Tippy.js library to add some tooltips to the buttons in the card
- Follow the installation instructions to install the library in your project
- Line 6 and 13: add the
data-tippy-content
attribute to the two buttons in the card - Line 7 and 14: add
class="outline-0"
to remove the outline around the buttons when they are clicked
php
<div class="flex justify-between border-t border-gray-300 bg-gray-100 px-4 py-2">
<div>{{ $record->price_euro }}</div>
<div class="flex space-x-4">
@if($record->stock > 0)
<button class="w-6 hover:text-red-900"
data-tippy-content="Add to basket<br><span class='text-red-300'>NOT IMPLEMENTED YET</span>">
<x-phosphor-shopping-bag-light class="outline-0" />
</button>
@else
<p class="font-extrabold text-red-700">SOLD OUT</p>
@endif
<button class="w-6 hover:text-red-900"
data-tippy-content="Show tracks">
<x-phosphor-music-notes-light class="outline-0" />
</button>
</div>
</div>
<div class="flex justify-between border-t border-gray-300 bg-gray-100 px-4 py-2">
<div>{{ $record->price_euro }}</div>
<div class="flex space-x-4">
@if($record->stock > 0)
<button class="w-6 hover:text-red-900"
data-tippy-content="Add to basket<br><span class='text-red-300'>NOT IMPLEMENTED YET</span>">
<x-phosphor-shopping-bag-light class="outline-0" />
</button>
@else
<p class="font-extrabold text-red-700">SOLD OUT</p>
@endif
<button class="w-6 hover:text-red-900"
data-tippy-content="Show tracks">
<x-phosphor-music-notes-light class="outline-0" />
</button>
</div>
</div>
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