Advanced Alpine.js, Part II: Moving Beyond HTML
13 December 2021
Posts in This Series
- Part I: The Story So Far
- In which we review some basics about Alpine.js
- Part II: Moving Beyond HTML (This post)
- In which we refactor our app and add filtering
- Part III
- Coming soon!
- Part IV
- Coming soon!
- Part V
- Coming soon!
In this series, we’ve been building a music playlist app using some modern workflows and Alpine.js features. Today, we’ll add a quality-of-life improvement for our users and get to know some of Alpine’s more advanced features. We’ll start by moving and refactoring our inline code into a separate JavaScript file, then add filtering so users can more easily navigate through the 250+ playlists. Let’s get started!
Moving Beyond HTML
Up to this point, we’ve used Alpine’s expressive inline code directives for all the behavior in our app. Though this is a perfectly fine way to continue, reading JavaScript inside HTML attributes gets more and more difficult over time. If you’re working with Alpine’s inline scripts in production, I’d encourage you to frequently revisit whether it’s time to move to a more formal script file. For our playlists app, now is that time!
Alpine provides a convenient global for injecting our formerly-inline scripts into its runtime, as well as a global document
event so we can ensure it’s completely loaded before attempting to do so:
document.addEventListener('alpine:init', () => { |
At this point, though, we have a choice of how to proceed: Continuing to use a data
object, or upgrading to a store
. These two concepts are fairly similar, and use nearly-identical APIs, supporting properties, functions, etc. The main difference is that Stores are globally-accessible in Alpine apps (via the $store
variable), while Data objects are designed to be more tightly-scoped and flexible.
Our straightforward app could quite easily rely on either approach, but we’ll take this opportunity to add a Store. In your apps, this is a great opportunity to think about whether you need to reuse these services, and how much of your app they affect. Logic included in specific parts of your app (think widgets, cards, and the like) are well-served by Data objects, while tracking global state like a user’s authentication status and preferences work well with Stores.
Okay, back to some code! After creating our new script file, let’s create a Store and add some basic functionality. In part one, we used Lodash and a window
global to group our playlists by a given property. We could continue to use Lodash’s response format (a single object with one key per group), but I think a proper array is more understandable, so we’ll dip into the native Object.entries()
function to clean that up, as well:
document.addEventListener('alpine:init', () => { |
We also need to update the HTML directives to reference our nascent Store. Our wrapper <template>
tag now looks like this:
<!-- `$store.app` is how we reference our Store by name. --> |
Hopefully you can already see the benefits of a separate script file: Putting our multi-step conversion from an object literal into an array of objects would be quite confusing to try to read in the middle of an HTML attribute, and that’s even assuming syntax highlighting works!
Now that we have a great place for more complex logic, let’s add some!
Our Feature Presentation
The first feature we’re going to add to our app is filtering. Our playlists collection comprises over 250 items, which is a lot to scroll through! Adding a quick filter would really help our users narrow down the potential mood playlists they can listen to. The Alpine docs already contain a tutorial on building a search input, so I won’t go into too much detail on how to build one, but the code is quite clear, regardless.
There are two behaviors I’d like to highlight, however: One, we’d still like our playlists grouped after they’re filtered, so we’re going to update the groupedPlaylists
property to group the filtered list instead of the complete one; and two, since we’re using each playlist’s slug
, we need to replace any hyphens with blank spaces in case a user searches a string like “chilling out”. The latter is easily accomplished with a regular expression.
Alpine.store('app', { |
As you might expect with a modern JavaScript library, Alpine handles all the reactivity for us! Just referencing this.filter
and friends automatically re-evaluates each property whenever the upstream values change.
At this point, we could play around with the filter
property to make sure the searching works, but it’s so easy to add the <input>
so users can perform the filter that we may as well! We’ll add the search filter input into the page’s <header>
; if you’ve been following along with the HTML, you’ll know that only our <ul>
is “Alpine-aware” via a x-data
attribute, so we can either add x-data
to the header element, or the HTML <body>
itself. I’ve chosen the latter, since this entire page now is one big app!
<body x-data> |
We now have a nice quick filter input that searches playlist names, and thanks to our updates to the groupedPlaylist
property, we don’t even have to deal with empty groups! If you’re feeling adventurous, though, a great UX improvement would be to display a “no results” message, but I’ll leave that as an exercise to the reader.
Until Next Time!
In this post, we refactored Alpine’s inline HTML directives into a separate script file, and added a quick filter so users can easily find playlists. Filtering is a great start to improving our app’s user experience, but we can go further: Allowing users to “favorite” mood playlists is a fantastic way to make our app even more usable! Stop by in our next post when we do just that, and we’ll look at another nifty component of Alpine while we’re at it. Happy listening!