Intro: Why Alpine.js for Blade Interactivity
Hello guys. If you have a long Laravel Blade page with a lot of dynamic calculations like, for example, pricing with checkboxes with this summary at the bottom for this dynamic JavaScript behavior, you don't necessarily need to use frameworks like Vue.js or ReactJS.
They are pretty heavy to install and complex to learn. Instead, you may want to take a look at an alternative called AlpineJS. It's pretty popular in the Laravel ecosystem and created by the same person who created Livewire, Caleb Porzio.
I will show you three AlpineJS examples in action with the code.
Example 1: Booking Reference Validation (Uppercase, Length, Border)
So here's a simple example. You enter booking reference and then it validates that it should be only uppercase letters, transforming everything to uppercase.
Also, it doesn't allow me to enter more than six characters. And also after I hit tab it validates if it is six characters; if it's not and I hit tab then the border is red.
We start typing and then something is happening in JavaScript and this is powered by this x-data. Basically, in whatever HTML element, div or in this case form, you may define x-data with variables and methods that would be available only inside of that HTML element.
In this case we define bookingReference as a variable property, assign the value of empty or it may come from the session after submitting the form, and then we define three functions and one of them is referencing another one. So it's this dot notation for both methods and variables to be referenced inside of the same Alpine block.
It's not even a component. It's a block of x-data which allows us then inside in any of the inputs or other elements to have x- Alpine syntax.
On that bookingReference we have three things. x-model bookingReference attaches/assigns the value of that input to the variable here in x-data, and then we have oninput we call that function from the same Alpine component.
The main thing about Alpine: it's all inline. You don't have to create separate JavaScript file, run npm commands, cross-reference different components. That's the thing. Alpine is for small, smallish interactions on the same Blade page.
We call the function which then transforms the value of bookingReference to uppercase using this syntax. And then also we bind the CSS classes of that component using x-bind:class. We have red when we have errors from the session after form submissions. Otherwise we have a class returned from a function in the same Alpine component or block.
So we return either gray border if it's empty, otherwise green or red border depending on whether the reference is valid. And that isValid is checked by another kind of private function with regular expression.
Again the demonstration: if I hit tab the border is red, but if I enter six characters the border is green. This is a simple example of Alpine.js interaction where you don't need to go out of that same Laravel Blade file to implement that JavaScript behavior.
Installing Alpine: CDN vs npm and Bootstrapping
First, to install AlpineJS into a Laravel project into Blade you have two options. First you can choose the CDN route like in this case, or if you want to do that properly with npm you can do npm install and then add three lines into either resources/js/bootstrap.js or resources/js/app.js.
In this case I separated those two and I will explain why when explaining a bit more complex example of Alpine.
For now, that's all you need to know about installation: npm install and then three lines of JavaScript. And then in your Blade, for example, let's take a look at step three Blade for a more complex example.
When you define Alpine data in resources.js/app.js using Alpine.data('seatSelection', ...), you define properties and functions and then you need to import and window.Alpine and then Alpine.data and only then call Alpine.start. Otherwise this may not be registered properly and in the browser console you'll get an error like 'seatSelection not defined.'
Example 2: Complex Seat Selection with JSON-driven Pricing
The next example of Alpine, a more complex one, comes from seat selection where you click on the seat and then it recalculates the additional fee.
That additional fee is actually quite complex. I already talked about pricing on this channel about the same project in a few videos in the days before. The price depends on whether the seat is near the window or in the middle or in this position. Also there are classes like business class or non-business. So the fee may be up to hundreds of dollars.
To show those things and style we have to pass quite a complex JSON object to Blade and then to Alpine. Here I am in my step three Blade file and we have a div with x-data. This is kind of the initialization of Alpine block, Alpine component. In here you don't see the variables and the functions immediately as you saw in the previous example because it's quite complex with a lot of things.
That's why in some cases it's beneficial to offload it somewhere else and that somewhere else, seatSelection, is defined in resources.js app.js as Alpine.data('seatSelection', ...). Here you define the properties, the functions, and this is init. It's kind of like a constructor and it takes the data from window.seatsData which we will get to in a minute.
If you define that separately you need to separate the bootstrapping of Alpine. This is importing and window.Alpine, then Alpine.data, and only then call Alpine.start.
Back to the Blade: a bit above you have the script to define the actual data, see window.seatsData, which is then parsed in init method to this.seats. Then later in Blade we have an x-for loop with seats and inside we have more complex structure. Those seats actually come from controller.
If we go to the controller and step three, we have seats as a parameter of return view. Seats is a structure of quite complicated JSON. If we dd we have items: array of 30 items. Each item in this array means one row of seating on the plane. We have six seats: 10A, 10B, and so on. All of that is passed to Blade first and then as JSON to Alpine.
In Alpine in that Blade, on the button, basically each seat is a button. We have @click with Alpine selectSeat and we have more Alpine syntax like x-text to show only the letter of that seat. For example, the letter only is shown, done in Alpine transforming the seat number.
Inside of that getSeatClass we have seat either available or type of business or economy — we have different colors and borders.
On click we have selectSeat which assigns the value to seatId and position. Then lower we have x-text for summary of what was chosen. x-text getSeatPrice and visually this corresponds to the block of selected seat with additional fee as the main thing.
To recap: you initialize the x-data with all the logic in, for example, resources/js/app.js, but you could do that inline in the same step three Blade. I wanted to show this as an example of a complex separate component of AlpineJS. Inside you can do things like x-for, @click, x-text and other Alpine syntax to have dynamic behavior on the same page without leaving the same Blade file — although in this case the logic is in app.js.
For such complex cases if you make any changes here then you would need to recompile with npm run dev or npm run build for those changes to take effect. For such complex cases Alpine may be on par with React or Vue because you would still create a separate component and add it to the Blade.
Example 3: Baggage and Pricing Calculator (In-blade Alpine Script)
The final third example is another way of using Alpine.js. We have a form of pricing — change some selects, some radios, some checkboxes, and we have calculation at the end.
In step four Blade we have a div with x-data and in this case baggageData is a function. It's not defined inline with variables and functions but instead it's defined at the very end of the same Blade file in a script section. This is another way to define the Alpine logic.
In that script we use Laravel and Blade syntax to add JSON-like variables. These are the properties of that Alpine component baggageData and then we have a function calculateTotal that references this.checkedBags, this.bagPricing everywhere.
In the Blade part we use x-text="calculateTotal" for the total, x-show="bagInsurance" which means show only if true or false, and x-show="specialItems.length > 0" which means the whole div is shown only in that condition. We also have x-text with JavaScript syntax and blocks with x-model. For example, specialItems has x-model with value fragileItems or musicalInstruments and so on.
SpecialItems is a JSON structure and is used in a for-each loop to calculate the total. x-model binds that to the property and then in live mode calculateTotal is called and refreshes the text on the same page.
In this case the logic of everything is in the same Blade file and it looks similarly to a Vue or React component with template and script; it's just that for the template we stay in regular Laravel Blade. From that point of view Alpine is more convenient, more native to Laravel, whereas Vue or React would be an additional separate mechanism.
Alpine vs Livewire and When to Use Server-side Updates
So far I've been talking about Alpine installed into a Laravel project, but actually you can also use Livewire. As I mentioned, it comes from the same author, Caleb Porzio, and in the documentation of Laravel Livewire there is a section on Alpine which says Livewire ships with Alpine out of the box. So there is no need to install it separately.
If you use Livewire in your project then there's no need to run npm install alpine — it will be installed automatically as part of Livewire. You may want to make that component a Livewire component instead of Laravel controller and then put all that logic of variables and functions inside Livewire within PHP if you're more comfortable with backend code instead of writing JavaScript.
With Alpine you still need to write plain JavaScript here and there and Livewire may be more convenient. But keep in mind that if you have those dynamic elements with Livewire, then it will call the server each time some checkbox is checked. For Alpine, it's all local in the browser without any server requests.
In some cases you may want server requests if you want to double-check the price if it hasn't changed. It depends on your situation. In fact, even in this project I tried to use Livewire for one of the steps, but then decided to move everything to Alpine just to have the same logic of six steps and not break the pattern of the code.
Speaking of those six steps: in the route I have the same controller with six steps, or in fact 12 routes, one for the form and one for processing that specific step of the form, and on each step I save part of the data to the database and then proceed to the next step.
In controller, for example processStepThree, we have separate services to save the data and then we redirect to route of step four. You would think that maybe all of that form could be one component whether it's a Livewire component or React or Vue or maybe one big Blade file with a lot of AlpineJS internally. It depends on your scenario and your project.
This form is pretty long with more than, I don't know, 100 components maybe. If we have it all in tabs of the same form and we don't save the form data right away, there is a risk of forcing the user to fill in the form from the very beginning next time.
Also the logic in each step is pretty complex and pretty long. So each page separately is its own entity, its own Blade with different layouts of inputs and checkboxes. I would personally have one component with a few tabs only if there's, well, a few tabs like up to 20 inputs maybe max and two or three tabs where it's quickly filled in and manageable.
In this case I decided to have separate methods in controllers, separate Blades, just using the same visual layout.
Resources, Premium Content, and Final Thoughts
My goal was to introduce you to AlpineJS examples with simple and complex cases. If you want a more structured introduction instead of just one example, I have a course on AlpineJS on Laravel Daily and the repository code for this example is also on Laravel Daily in the project examples section. Both of those are for premium members.
Before ending, I want to address a YouTube comment that my videos are mostly just a preview for a course. I understand where it comes from. I'm trying to find the balance between free and paid content. Maybe in some videos I overemphasize mentioning my courses or repository is premium and maybe it feels like the video is just a trailer.
What I'm trying to do is earn enough money from my content to be able to do both premium versions and some free versions on YouTube. In these videos I often show most of the code so you can figure out how to do that in your case. Premium members get more structured value. It's almost the same information, just more structured and convenient.
If you think my videos are trailers and you don't receive value from my YouTube videos, then don't watch them. If you want to discuss Alpine or have questions or disagree with some points, let's discuss everything in the comments below. That's it for this time and see you guys in other.