Introduction to Alpine.js
Alpine.js: your new lightweight javascript framework. Why is it? Well, compared with other javascript frameworks such as Vue, React, Svelte or Solid, it has a very specific use case. It enables you to easily add behavior and interactivity on HTML pages rendered on the server. So there are no single page application constraints, no hydration, and no server-side rendering solution. It's a minimal framework that allows you to do the bare minimum work with javascript on your page in a structured manner.
It's an appropriate tool to be integrated in existing legacy projects that rely on vanilla javascript or jQuery, while also being a good fit for usage in new apps where all the rendering and the heavy lifting is done by a framework such as Spring or Rails. In this video, we'll simulate the scenario in which we receive from the server a static HTML page for our online shopping app. In here, we'll add the JS behavior needed to allow customers to add the t-shirts to a shopping cart. Without further ado, let's jump into it.
Project Setup and Adding Alpine
I created a small Deno server which serves static files and HTML pages directly from the disk. Usually, you'd compute the resulting HTML on the server using information fetched from a data source. This setup is pretty straightforward. However, you can check out the video I'm linking in the top right corner if you are interested in finding more about Deno.
As a response from the server, you'll get an SEO-friendly static HTML which is easily crawlable by the search engines. Furthermore, this is plain HTML directly rendered in the page, so you'll have excellent metrics such as the first meaningful paint. With the HTML in place, you'll need to add some JS behavior to allow your users to interact with the DOM. We'll do this by simply adding the Alpine.js script tag at the end of our HTML page.
Core API Concepts & x-data
Okay, now Alpine is famously simple. Its whole API has 15 attributes, six properties, and two methods. As a suggested approach, you are encouraged to compose behavior directly in the HTML markup using its attributes. However, one of my rules of thumb is to always attempt at least some basic separation of concerns, so I'll do most of the JS work in a separate file called index.js. Regardless of my decision, note that most of the things we are going to do can be achieved by working directly in the markup using the x-text.
In a newly created JS file, I'm going to register an event listener, which will be triggered when the Alpine library is loaded in the page. This is the main entry point for our behavior definition, and this is where it's safe for us to perform work using the library's API.
Building the Product Component
The x-data attribute is what makes a DOM element and its children interactive. In short, this block of HTML will start behaving as a component. I'll add the x-data attribute on the product section. This is the main section of the page, and there are a few things we want to make interactive. The customer should be able to choose the t-shirt color, fill in the number of items he wants, and then click on the blue button to add the item to the cart.
Inside the JS file, using Alpine.data, we can link the state and behavior to the product component. We need a color and a quantity to keep track of the user's decision. Then, whenever the color is changed, we can easily update the component state by assigning the new value. Note the use of the this keyword, which is very important here.
Back in the HTML markup, we can easily register event handlers pointing to our method defined earlier. Note that these are child elements of the parent product component, so anything defined under the product section will have access to the state and behavior we declared earlier in the Alpine data method. I'm also using the bind shortened attribute to conditionally add the selected class and outline the user selection. So far, we've seen the basics of event handling and the fact that the UI is automatically updated when something in the component state changes. Alpine also supports two-way data binding on input fields with the x-model attribute. Whenever the user will make a change in the input field, the update will be immediately propagated in the component state.
Global State with Alpine Stores
To finish this, we need to implement the actual add to cart functionality. For this, however, I'm going to use an Alpine store. You are probably familiar with the concept of state management and the difficulties of passing data reliably through the application. Alpine offers a simple but powerful solution for this, which will allow us to display the cart content in one component while being able to update the cart from sibling components.
The implementation of the cart store is pretty straightforward. I'm defining a list of products and an add method to push data into the list. Of course, this is a pretty naive implementation; in a real-life scenario, you'd do some fetch calls to a REST API and probably better manage things such as quantities and prices.
Next, I'm making the header section a component using the x-data attribute. We'll do the same thing as we did for the product component. When I'm clicking on the cart link, I want to see the added products displayed in the dropdown.
Connecting Components & Rendering Lists
With all this in place, it's time now to get back to the HTML and put everything together. In the header, we can easily display the number of products in the cart using the store property. The x-show and x-data attributes are pretty self-explanatory. As I mentioned earlier, Alpine offers 16 such useful attributes which will basically cover all your needs when it comes to interacting with the DOM. When clicking on the Add button, we can now simply call the cart store method to push data in the list. All components referencing the store will be then updated.
I need to add two more getter methods in the cart store for simplicity reasons, and now we can fully test our workflow. We can reliably pick up colors, modify the quantity, and add the items to the cart. As a result, the cart counter updates accordingly in the header. In the store, we have a products list we can iterate over to render the cart contents in the DOM. This can be done with the x-for attribute. As a caveat, you should always use the template HTML tag when using x-for. We make sure the dropdown is toggled when the cart button is clicked. One important behavior of dropdowns is that they get automatically closed when clicking anywhere in the page. Alpine offers directive modifiers to customize the behavior of event listeners. We can use the .outside modifier for our current use case.
Component Scope and Targeting
With the main product section out of the way, let's quickly add some behavior for the related section as well. I'm not planning to do anything fancy here, so let's just make sure more items can be added in the cart when the related products are clicked. However, we'll be in for a little surprise. Clicking on the items does not trigger the event listener. The reason for this is that the related section is outside of the components we worked with so far, so Alpine doesn't know this section of code exists. To fix the problem, we'll need to add the x-data attribute on the section element. Note that we are not passing in a name or any other information; we are simply making this an Alpine component, and the framework will take care of the rest. It should be clear now that Alpine can be very targeted, and you can focus it only on a small part of your HTML. This is what makes the framework such a good solution to be integrated in existing, large legacy projects.
Nested Components & the Persist Plugin
Another thing I want to discuss is the concept of nested components. So I'm going to wrap the whole application in an app component. It's important to know that child components will still maintain their scope, but they can also access information from the parent scope. Now we'll work on allowing users to switch the dark mode on or off, which will give us the chance to discuss nesting in more detail, but also touch on the subject of Alpine plugins.
I want the user's decision to select the dark theme to persist even though the page is refreshed. We can do this easily by adding the persist plugin script into our HTML. This will allow us to make properties from the component state persist into the local storage by default. Next time the page is opened, Alpine will know to correctly populate the state props with the values from the local storage. I'm also adding a button in the header component to allow us to toggle between the light and the dark mode. And now we should be running into a new error.
Debugging this and Preventing UI Flicker
What this new issue tells us is that this keyword is not actually pointing to the correct Alpine instance. If you have experience with javascript, you are probably familiar with the history of the this keyword and the various binding issues you can run into. Some frameworks such as React or Solid are great at abstracting away some of the weirdness of javascript, but for Alpine, we still need to be careful with this. To fix the problem, we need to change the app.data second parameter into a function instead of an arrow function. Note that we can call the dark mode toggle function from the header component even though the function was defined in the parent app component.
As a final step for this feature, I'm conditionally rendering a class which will apply the correct styles for the entire app. When I'm refreshing the page, the dark mode selection will stick, but you'll see a small flicker caused by the time needed for Alpine to load and run on the page. We can easily fix this by using the x-cloak attribute, which will make sure that everything is hidden on the page until Alpine is loaded and ready.
Lifecycle Hooks & Watchers (x-init, $watch)
In short, the attribute will be removed by the framework, and until then, the display: none CSS rule will be in effect. All big libraries have some sort of mechanism that allows you to run code at different times in the component life cycle. Alpine allows you to run code when a component is initialized with the x-init tag. We'll use this hook to take a look at another interesting feature: watchers. The $watch magic method allows you to monitor any property managed by Alpine.
In our scenario, I want to display a small confirmation message whenever the cart products array changes. So whenever the change happens, the onProductAdded function will be executed. I'll take this opportunity to mention another magic property: $data. This gives us access to the current Alpine data scope. As I already mentioned, since we are using this keyword, we need to be careful to correctly bind the function to the correct scope. Once we are done with that, the watcher should trigger the callback when the add to cart button is clicked. After a couple of seconds, the setTimeout will also be executed and the confirmation message will be hidden.
Conclusion and Final Thoughts
And this pretty much sums up our incursion into Alpine.js. Hopefully, you should have a clear idea now about both its capabilities and its use cases. All in all, I think it's a useful tool to keep in mind for some specific projects, especially because of its lightweight API. If you've made it this far, please consider liking this video, adding a comment, and subscribing to the channel. Thanks for watching.