Yesterday, the Laravel staff launched Laravel Folio – a robust page-based router designed to simplify routing in Laravel functions. Right this moment, they released Volt -an elegantly crafted purposeful API for Livewire, permitting a part’s PHP logic and Blade templates to coexist in the identical file with lowered boilerplate.
Though they could be used individually, I believe utilizing them collectively is a brand new, extremely productive technique to construct Laravel apps.
On this article, I’ll train you methods to construct a easy app that lists out episodes of the Laravel Information podcast and permits customers to play them, with a participant that may seamlessly proceed enjoying throughout web page hundreds.
Setup Livewire, Volt, and Folio
To get began, we have to create a brand new Laravel app and set up Livewire, Volt, Folio, and Sushi (to make some dummy knowledge).
laravel new
composer require livewire/livewire:^3.0@beta livewire/volt:^1.0@beta laravel/folio:^1.0@beta calebporzio/sushi
Livewire v3, Volt, and Folio are all nonetheless in beta. They need to be fairly secure, however use them at your individual danger.
After requiring the packages, we have to run php artisan volt:set up
and php artisan folio:set up
. This may scaffold out some folders and repair suppliers Volt and Folio want.
The Episode
mannequin
For dummy knowledge, I will create a Sushi mannequin. Sushi is a package deal written by Caleb Pozio that permits you to create Eloquent fashions that question their knowledge from an array written instantly within the mannequin file. This works nice if you’re constructing instance apps or have knowledge that does not want to alter fairly often.
Create a mannequin, then take away the HasFactory
trait and change it with the Sushi
trait. I added the small print of the 4 newest Laravel Information Podcast episodes as the information for this instance.
I will not go into element on how all this works since this is not the purpose of the article, and you may doubtless use an actual Eloquent mannequin when you have been to construct your individual podcast participant.
<?php
namespace AppModels;
use IlluminateDatabaseEloquentModel;
use SushiSushi;
class Episode extends Mannequin
use Sushi;
protected $casts = [
'released_at' => 'datetime',
];
protected $rows = [
[
'number' => 195,
'title' => 'Queries, GPT, and sinking downloads',
'notes' => '...',
'audio' => 'https://media.transistor.fm/c28ad926/93e5fe7d.mp3',
'image' => 'https://images.transistor.fm/file/transistor/images/show/6405/full_1646972621-artwork.jpg',
'duration_in_seconds' => 2579,
'released_at' => '2023-07-06 10:00:00',
],
[
'number' => 194,
'title' => 'Squeezing lemons, punching cards, and bellowing forges',
'notes' => '...',
'audio' => 'https://media.transistor.fm/6d2d53fe/f70d9278.mp3',
'image' => 'https://images.transistor.fm/file/transistor/images/show/6405/full_1646972621-artwork.jpg',
'duration_in_seconds' => 2219,
'released_at' => '2023-06-21 10:00:00',
],
[
'number' => 193,
'title' => 'Precognition, faking Stripe, and debugging Blade',
'notes' => '...',
'audio' => 'https://media.transistor.fm/d434305e/975fbb28.mp3',
'image' => 'https://images.transistor.fm/file/transistor/images/show/6405/full_1646972621-artwork.jpg',
'duration_in_seconds' => 2146,
'released_at' => '2023-06-06 10:00:00',
],
[
'number' => 192,
'title' => 'High octane, sleepy code, and Aaron Francis',
'notes' => '...',
'audio' => 'https://media.transistor.fm/b5f81577/c58c90c8.mp3',
'image' => 'https://images.transistor.fm/file/transistor/images/show/6405/full_1646972621-artwork.jpg',
'duration_in_seconds' => 1865,
'released_at' => '2023-05-24 10:00:00',
],
// ...
];
The structure view
We’ll want a structure file to load Tailwind, add a emblem, and add some fundamental styling. Since Livewire and Alpine mechanically inject their scripts and kinds now, we do not even have to load these within the structure! We’ll create the structure as an nameless Blade part at sources/views/parts/structure.blade.php
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta identify="viewport" content material="width=device-width, initial-scale=1.0" />
<title>Laravel Information Podcast Participant</title>
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
</head>
<physique class="min-h-screen bg-gray-50 font-sans text-black antialiased">
<div class="mx-auto max-w-2xl px-6 py-24">
<a
href="/episodes"
class="mx-auto flex max-w-max items-center gap-3 font-bold text-[#FF2D20] transition hover:opacity-80"
>
<img
src="/photos/emblem.svg"
alt="Laravel Information"
class="mx-auto w-12"
/>
<span>Laravel Information Podcast</span>
</a>
<div class="py-10"> $slot </div>
</div>
</physique>
</html>
The episode checklist web page
First, we’d like a web page to show all of the episodes of the podcast.
Utilizing Folio, we are able to simply create a brand new web page within the sources/views/pages
listing, and Laravel will mechanically create a route for that web page. We would like our path to be /episodes
so we are able to run php artisan make:folio episodes/index
. That can create a clean view at sources/views/pages/episodes/index.blade.php
.
On this web page, we’ll insert the structure part, then loop over all of the podcast episodes. Volt gives namespaced features for many of the Livewire options. Right here, we’ll open common <?php ?>
open and shut tags. Inside these, we’ll use the computed
operate to create an $episodes
variable that runs a question to get all of the Episode fashions ($episodes = computed(fn () => Episode::get());
). We will entry the computed property within the template utilizing $this->episodes
.
I additionally created a $formatDuration
variable that is a operate to format every episode’s duration_in_seconds
property to a readable format. We will name that operate within the template utilizing $this->formatDuration($episode->duration_in_seconds)
.
We additionally have to wrap the dynamic performance on the web page within the @volt
directive to register it as an “nameless Livewire part” throughout the Folio web page.
<?php
use AppModelsEpisode;
use IlluminateSupportStringable;
use operate LivewireVoltcomputed;
use operate LivewireVoltstate;
$episodes = computed(fn () => Episode::get());
$formatDuration = operate ($seconds)
return str(date('Gh im ss', $seconds))
->trim('0h ')
->explode(' ')
->mapInto(Stringable::class)
->every->ltrim('0')
->be part of(' ');
;
?>
<x-layout>
@volt
<div class="rounded-xl border border-gray-200 bg-white shadow">
<ul class="divide-y divide-gray-100">
@foreach ($this->episodes as $episode)
<li
wire:key=" $episode->quantity "
class="flex flex-col items-start gap-x-6 gap-y-3 px-6 py-4 sm:flex-row sm:items-center sm:justify-between"
>
<div>
<h2>
No. $episode->quantity - $episode->title
</h2>
<div
class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-gray-500"
>
<p>
Launched:
$episode->released_at->format('M j, Y')
</p>
·
<p>
Length:
$this->formatDuration($episode->duration_in_seconds)
</p>
</div>
</div>
<button
kind="button"
class="flex shrink-0 items-center gap-1 text-sm font-medium text-[#FF2D20] transition hover:opacity-60"
>
<img
src="/photos/play.svg"
alt="Play"
class="h-8 w-8 transition hover:opacity-60"
/>
<span>Play</span>
</button>
</li>
@endforeach
</ul>
</div>
@endvolt
</x-layout>
The episode participant
From there, we have to add some interactivity. I wish to add an episode participant so we are able to take heed to the episodes from the episode checklist. This could be a common Blade part we render within the structure file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta identify="viewport" content material="width=device-width, initial-scale=1.0" />
<title>Laravel Information Podcast Participant</title>
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
</head>
<physique class="min-h-screen bg-gray-50 font-sans text-black antialiased">
<div class="mx-auto max-w-2xl px-6 py-24">
<a
href="/episodes"
class="mx-auto flex max-w-max items-center gap-3 font-bold text-[#FF2D20] transition hover:opacity-80"
>
<img
src="/photos/emblem.svg"
alt="Laravel Information"
class="mx-auto w-12"
/>
<span>Laravel Information Podcast</span>
</a>
<div class="py-10"> $slot </div>
<x-episode-player />
</div>
</physique>
</html>
We will create that part by including a sources/views/parts/episode-player.blade.php
file. Contained in the part, we’ll add an <audio>
factor with some Alpine code to retailer the energetic episode and a operate that updates the energetic episode and begins the audio. We’ll solely present the participant if an energetic episode is about, and we’ll add a pleasant fade transition to the wrapper.
<div
x-data="
activeEpisode: null,
play(episode)
this.activeEpisode = episode
this.$nextTick(() =>
this.$refs.audio.play()
)
,
"
x-show="activeEpisode"
x-transition.opacity.period.500ms
class="fastened inset-x-0 bottom-0 w-full border-t border-gray-200 bg-white"
type="show: none"
>
<div class="mx-auto max-w-xl p-6">
<h3
x-text="`Enjoying: No. $activeEpisode?.quantity - $activeEpisode?.title`"
class="text-center text-sm font-medium text-gray-600"
></h3>
<audio
x-ref="audio"
class="mx-auto mt-3"
:src="activeEpisode?.audio"
controls
></audio>
</div>
</div>
If we reload the web page, we do not see any modifications. That is as a result of we have not added a technique to play episodes. We’ll use occasions to speak from our Livewire parts to the participant. First, within the participant, we’ll add x-on:play-episode.window="play($occasion.element)"
to hear for the play-episode
occasion on the window, then name the play
operate.
<div
x-data="
activeEpisode: null,
play(episode)
this.activeEpisode = episode
this.$nextTick(() =>
this.$refs.audio.play()
)
,
"
x-on:play-episode.window="play($occasion.element)"
...
>
<!-- ... -->
</div>
Subsequent, again within the episodes/index
web page, we’ll add a click on listener on the play buttons for every episode. The buttons will dispatch the play-episode
occasion, which will probably be acquired by the episode participant and dealt with there.
<button
x-data
x-on:click on="$dispatch('play-episode', @js($episode))"
...
>
<img
src="/photos/play.svg"
alt="Play"
class="h-8 w-8 transition hover:opacity-60"
/>
<span>Play</span>
</button>
The episode particulars web page
Subsequent, I might like so as to add an episode particulars web page to show every episode’s present notes and different particulars.
Folio has some fairly cool conventions for route mannequin binding in your filenames. To make an equal route for /episodes/episode:id
create a web page at sources/views/pages/episodes/[Episode].blade.php
. To make use of a route parameter aside from the first key, you need to use the [Model:some_other_key].blade.php
syntax in your filename. I wish to use the episode quantity within the URL, so we’ll create a file at sources/views/pages/episodes/[Episode:number].blade.php
.
Folio will mechanically question the Episode fashions for an episode with the quantity we move within the URL and make that obtainable as an $episode
variable in our <?php ?>
code. We will then convert that to a Livewire property utilizing Volt’s state
operate.
We’ll additionally embody a play button on this web page so customers can play an episode whereas viewing its particulars.
<?php
use IlluminateSupportStringable;
use operate LivewireVoltstate;
state(['episode' => fn () => $episode]);
$formatDuration = operate ($seconds)
return str(date('Gh im ss', $seconds))
->trim('0h ')
->explode(' ')
->mapInto(Stringable::class)
->every->ltrim('0')
->be part of(' ');
;
?>
<x-layout>
@volt
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow">
<div class="p-6">
<div class="flex items-center justify-between gap-8">
<div>
<h2 class="text-xl font-medium">
No. $episode->quantity -
$episode->title
</h2>
<div
class="mt-1 flex items-center gap-3 text-sm text-gray-500"
>
<p>
Launched:
$episode->released_at->format('M j, Y')
</p>
·
<p>
Length:
$this->formatDuration($episode->duration_in_seconds)
</p>
</div>
</div>
<button
x-on:click on="$dispatch('play-episode', @js($episode))"
kind="button"
class="flex items-center gap-1 text-sm font-medium text-[#FF2D20] transition hover:opacity-60"
>
<img
src="/photos/play.svg"
alt="Play"
class="h-8 w-8 transition hover:opacity-60"
/>
<span>Play</span>
</button>
</div>
<div class="prose prose-sm mt-4">
!! $episode->notes !!
</div>
</div>
<div class="bg-gray-50 px-6 py-4">
<a
href="/episodes"
class="text-sm font-medium text-gray-600"
>
← Again to episodes
</a>
</div>
</div>
@endvolt
</x-layout>
Now, we have to hyperlink to the small print web page from the index web page. Again within the episodes/index
web page, let’s wrap every episode’s <h2>
in an anchor tag.
@foreach ($this->episodes as $episode)
<li
wire:key=" $episode->quantity "
class="flex flex-col items-start gap-x-6 gap-y-3 px-6 py-4 sm:flex-row sm:items-center sm:justify-between"
>
<div>
<a
href="/episodes/ $episode->quantity "
class="transition hover:text-[#FF2D20]"
>
<h2>
No. $episode->quantity -
$episode->title
</h2>
</a>
</div>
-- ... --
</li>
@endforeach
SPA-mode
We’re virtually there. The app seems fairly good and features properly, however there’s one challenge. If a consumer is listening to an episode, and navigates to a special web page, the episode participant loses its energetic episode state and disappears.
Fortunately, Livewire has the wire:navigate
and the @persist
directive to assist with these issues now!
In our structure file, let’s wrap the brand and episode participant in @persist
blocks. Livewire will detect this and skip re-rendering these blocks after we change pages.
<!DOCTYPE html>
<html lang="en">
...
<physique class="min-h-screen bg-gray-50 font-sans text-black antialiased">
<div class="mx-auto max-w-2xl px-6 py-24">
@persist('emblem')
<a
href="/episodes"
class="mx-auto flex max-w-max items-center gap-3 font-bold text-[#FF2D20] transition hover:opacity-80"
>
<img
src="/photos/emblem.svg"
alt="Laravel Information"
class="mx-auto w-12"
/>
<span>Laravel Information Podcast</span>
</a>
@endpersist
<div class="py-10"> $slot </div>
@persist('participant')
<x-episode-player />
@endpersist
</div>
</physique>
</html>
Lastly, we have to add the wire:navigate
attribute to all of the hyperlinks by way of the app. For instance:
<a
href="/episodes/ $episode->quantity "
class="transition hover:text-[#FF2D20]"
wire:navigate
>
<h2>
No. { $episode->quantity } -
$episode->title
</h2>
</a>
If you use the wire:navigate
attribute, behind the scenes, Livewire will fetch the brand new web page’s contents utilizing AJAX, then magically swap out the contents in your browser with out doing a full web page reload. This makes web page hundreds really feel extremely quick and permits options like persist to work! It permits options that beforehand you might solely accomplish by constructing a SPA.
Conclusion
This was a very enjoyable demo app to construct whereas studying Volt and Folio. I’ve uploaded the demo app right here if you wish to see the complete supply code or attempt it out your self!
What do you suppose? Is Livewire v3 + Volt + Folio the best stack for constructing Laravel apps now? I believe it is actually cool and would possibly really feel extra acquainted to people who find themselves used to constructing apps in JavaScript frameworks like Subsequent.js and Nuxt.js. It is also good to have all of your code for a web page collocated – styling (through Tailwind), JS (through Alpine), and backend code multi function file. Ship me your ideas on Twitter!