Appearance
Shop: Master Section
REMARKS
Because this is our first full-blown Livewire component in this course, we will split the solution into 3 parts:
- Part 1: Let's start with the "master page" where all the records are shown on a card and add some pagination (this chapter).
- 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 involves concepts like managing a shopping cart state, which is a bit more advanced and will be explored in a later chapter.
With our custom application layout established, we can now begin constructing the core functionality of the vinyl shop. This chapter focuses on building the main display area – the "master section". We'll create the Livewire component responsible for fetching and displaying vinyl records from our database. These records will be presented using a card-based layout, and we'll implement pagination to efficiently handle a potentially large collection. This initial setup forms the essential foundation upon which we will build more interactive features, such as viewing record details and applying filters, in the subsequent chapters.
Preparation
Before we can display any records, we need to establish the fundamental building blocks for this feature. This involves creating the Livewire component itself, defining a web route to make it accessible via a URL, and ensuring our new shop page utilizes the application's main layout that we previously constructed.
Create the Shop Component
Livewire components encapsulate both the server-side logic and the front-end presentation. They consist of a PHP class, which handles data fetching and user interactions, and a corresponding Blade view, which defines the HTML structure.
- Open your terminal in the project directory.
- Execute the following Artisan command to generate a new Livewire component named
Shop
:
bash
php artisan livewire:make Shop
1
This command conveniently creates two essential files for us:
app/Livewire/Shop.php
: This is the component's PHP class. It acts like a controller. Initially, it contains arender()
method, which is responsible for returning the view that should be displayed. We will modify this method to fetch our record data.resources/views/livewire/shop.blade.php
: This is the component's Blade view file. It starts as an empty canvas where we will define the HTML markup for displaying the records.
Add a New Route
To allow users to navigate to our shop page, we must define a route in Laravel's routing system. This route maps a specific URL path to our newly created Shop
Livewire component.
- Open the
routes/web.php
file, which contains the web routes for your application. - Add a new route definition that maps the
/shop
URL path directly to theApp\Livewire\Shop
component class. We'll also assign the route a name,shop
, which allows us to easily generate URLs to this page elsewhere in the application using theroute()
helper function.
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::get('itunes', Itunes::class)->name('itunes');
1
2
3
4
5
6
2
3
4
5
6
Now that the route exists, let's link to it from our main navigation bar.
- Open the
resources/views/components/layouts/vinylshop/navbar.blade.php
file, which contains the navigation structure. - Locate the
flux:navlist.item
for the "Shop" link. - Update the
href
attribute to use theroute()
helper with the name we just assigned:{{ route('shop') }}
.
php
<flux:navlist variant="outline">
<flux:navlist.item icon="disc-3" href="{{ route('shop') }}">Shop</flux:navlist.item>
<flux:navlist.item icon="envelope" href="{{ route('contact') }}">Contact</flux:navlist.item>
<flux:navlist.item icon="shopping-cart" href="#">Basket</flux:navlist.item>
</flux:navlist>
1
2
3
4
5
2
3
4
5
Update the view
With the component and route in place, let's add some initial content to the shop.blade.php
view. We'll set the page title and description using named slots (x-slot:title
and x-slot:description
), which our main application layout ( x-layouts.vinylshop
) will render. We will also scaffold the basic HTML structure for a single record card, which we'll later use within a loop.
- app/Livewire/Shop.php:
- (no changes needed here yet, just be aware of it).
- resources/views/livewire/shop.blade.php.
- Add the
<x-slot>
directives for the title and description. - Add the placeholder divs for the filter section (which we'll implement later).
- Include a
<flux:separator>
for visual division. - Add the basic structure for the master section, including the container
div
and the initial card layout using Tailwind CSS classes for styling and responsiveness. Use a placeholder image for now. - Include the
<x-itf.livewire-log/>
component at the end for debugging purposes during development.
- Add the
php
<?php
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
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Tailwind @container
class
Notice the div
wrapping our grid of cards has the @container
class applied. This is a powerful feature of Tailwind CSS known as Container Queries. Instead of making layout decisions based solely on the browser's viewport width (like traditional responsive design with sm:
, md:
, lg:
prefixes), @container
allows elements to adapt based on the width of their parent container. In our shop.blade.php
view, the classes @5xl:grid-cols-2
and @7xl:grid-cols-3
mean the grid will switch to 2 columns when the container itself reaches the 5xl
size breakpoint defined in Tailwind, and 3 columns at the 7xl
breakpoint, regardless of the overall page width. This makes components more reusable and self-contained. You can learn more about this in the Tailwind CSS Container Queries documentation.
Get and Display Records
Now we'll breathe life into our shop page by fetching actual record data from the database and displaying it. The primary responsibility of the render()
method within our Shop.php
component class is to prepare any data that the corresponding Blade view (shop.blade.php
) needs.
We will use Laravel's Eloquent ORM to interact with our database. Inside the render()
method, we'll query the Record
model to retrieve the records. Initially, we'll order them alphabetically by artist and then by title using the orderBy()
method. For now, we use ->get()
to retrieve all matching records as a collection. This collection is then passed to the Blade view using the compact()
function, which creates an associative array where the key is the variable name ('records') and the value is the variable's content ($records
).
Next, we need to modify the shop.blade.php
view to iterate over this $records
collection and render a card for each record using the structure we previously defined.
- app/Livewire/Shop.php:
- Inside the
render()
method: - Import the
App\Models\Record
class at the top. - Query the
Record
model, applyingorderBy('artist')
andorderBy('title')
. - Use
->get()
to fetch the results. - Pass the
$records
variable to the view usingcompact('records')
.
- Inside the
- resources/views/livewire/shop.blade.php:
- Wrap the card's main
div
element (the one with theflex border...
classes) inside a Blade@foreach($records as $record)
loop. - Add the
wire:key="{{ $record->id }}"
directive to this maindiv
inside the loop. This is crucial for Livewire's performance. It provides a unique, stable identifier for each record in the list, allowing Livewire to efficiently track changes and update only the necessary parts of the DOM instead of re-rendering the entire list. Using the record's primary key (id
) is a standard and reliable practice here. - Replace the placeholder content within the card (image
src
,alt
,title
, artist name, record title, genre name) with dynamic data from the$record
object available inside the loop (e.g.,{{ $record->artist }}
,{{ $record->title }}
,{{ $record->cover }}
). - Format the price using Laravel's
Number::currency()
helper function. This provides robust and locale-aware currency formatting. We specify the currency code ('EUR') and the locale ('nl' for Dutch formatting in this example). - Pass the fetched
$records
collection to our debugging component by adding the:records="$records"
attribute to<x-itf.livewire-log>
. This allows the log sidebar to display the properties of the records currently being rendered.
- Wrap the card's main
php
<?php
namespace App\Livewire;
use App\Models\Record;
use Livewire\Component;
class Shop extends Component
{
public function render()
{
$records = Record::orderBy('artist')
->orderBy('title')
->get();
return view('livewire.shop', compact('records'));
}
}
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
Update the Order Button
To provide clearer feedback to the user, let's visually distinguish the "Add to basket" button based on the record's stock availability. If a record is out of stock (stock = 0
), we'll change its appearance to indicate this state. We'll change the icon color to red and update the tooltip text.
- resources/views/livewire/shop.blade.php:
- Locate the
<flux:button>
for adding to the basket within the@foreach
loop. - Wrap this button and add a corresponding "out of stock" version within a Blade
@if/@else
block. - The
@if
condition should check$record->stock > 0
. - In the
@else
block (when stock is 0 or less): - Keep the same
flux:button
structure. - Change the
tooltip
attribute to "Out of stock". - Add Tailwind utility classes to change the text color, for example:
!text-red-200 dark:!text-red-700/75
. The!
prefix marks the utility as important, potentially overriding other styles.
- Locate the
php
<div
class="flex items-center justify-between border-t border-zink-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 px-4 py-2">
<div class="flex-1">{{ Number::currency($record->price, in: 'EUR', locale: 'nl') }}</div>
<div class="flex space-x-2">
@if($record->stock > 0)
<flux:button
icon="shopping-bag" tooltip="Add to basket" variant="subtle"
class="cursor-pointer border border-zinc-200 dark:border-zinc-700"/>
@else
<flux:button
icon="shopping-bag" tooltip="Out of stock" variant="subtle"
class="cursor-pointer border border-zinc-200 dark:border-zinc-700 !text-red-200 dark:!text-red-700/75"/>
@endif
<flux:button
icon="musical-note" tooltip="Show tracks" variant="subtle"
class="cursor-pointer border border-zinc-200 dark:border-zinc-700"/>
</div>
</div>
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
Pagination
Fetching and displaying all records from the database at once is inefficient, especially as the collection grows. It leads to slower page loads and can overwhelm the user. Laravel provides a robust pagination system, and Livewire integrates with it seamlessly, allowing us to load and display records in manageable chunks (pages) with minimal effort.
Add Pagination to the Component
To enable pagination, we need to make adjustments in both the Livewire component class and its Blade view.
- app/Livewire/Shop.php:
- Introduce a public property named
$perPage
. This property will determine how many records are displayed on each page. Initialize it with a sensible default value, such as6
. - Import the
Livewire\WithPagination
trait using ause
statement at the top of the file. - Include the trait within the class definition by adding
use WithPagination;
. This trait provides the necessary methods and properties for Livewire to handle pagination state and rendering. - Modify the Eloquent query in the
render()
method. Replace the->get()
call, which fetches all records, with->paginate($this->perPage)
. This tells Eloquent to fetch only the records for the current page, based on the$perPage
value, and it also automatically retrieves the necessary information to render pagination links (like total record count, current page, etc.).
- Introduce a public property named
- resources/views/livewire/shop.blade.php:
- Add the
$records->links()
method call within the view. This method, available on the paginator instance returned by->paginate()
, renders the HTML for the pagination navigation links (e.g., page numbers, next/previous buttons). It's common practice to place this both above and below the list of items for easier navigation.
- Add the
php
<?php
namespace App\Livewire;
use App\Models\Record;
use Livewire\Component;
use Livewire\WithPagination;
class Shop extends Component
{
use WithPagination;
public $perPage = 6;
public function render()
{
$records = Record::orderBy('artist')
->orderBy('title')
->paginate($this->perPage); // Replace get() with paginate()
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Customizing Pagination Views
Livewire uses Laravel's default pagination views. While functional, their default styling (based on Tailwind CSS) might not perfectly match our application's design, particularly the highlighting for the active page. Livewire allows us to easily customize these views.
First, we need to publish Livewire's default pagination views into our project's resources
directory. This makes them available for modification.
- Open your terminal in the project root.
- Run the following Artisan command:
bash
php artisan livewire:publish --pagination
1
This command copies Livewire's built-in pagination view files into the resources/views/vendor/livewire
directory. You will find several files corresponding to different CSS frameworks (like Bootstrap) and simple versions. Since our project using Tailwind CSS, we only need to modify the Tailwind-specific view.
- resources/views/livewire/shop.blade.php:
- Locate the HTML element responsible for rendering the currently active page number at line 91.
- Modify the classes applied to this active page element to match our styling:
- Change
text-gray-500
(or similar) totext-white
. - Change
bg-white
(or similar) tobg-zinc-800
(or your preferred dark background color class).
- Change
php
````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-zinc-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
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17