The innovation in front-end JavaScript frameworks is one of the great techno-cultural phenomena of our time. For over 20 years now, we have witnessed a long tail of evolutionary creativity unfold. Each new idea goes into the communal pot, stirring up improvements in both the process of developing software and the end products that developers build.
One of the frameworks making a name for itself these days is Alpine.js. Alpine is a minimalist framework fashioned, as its name implies, for light handling over rugged terrain. It delivers a lot of power in a lean, easily mastered package. This article will give you a taste of Alpine.js, so you can understand what it delivers and when it might be useful to you.
Alpine’s minimalist API
As the Alpine.js documentation describes it, Alpine’s API is a collection of 15 attributes, six properties, and two methods. That’s a very small API profile. Its minimalist purpose is to provide reactivity in a clean format, augmented with some surrounding niceties like eventing and a central store.
Consider the very simple web page in Listing 1.
Listing 1. A simple web page built with Alpine.js
<html>
<head>
<script src="https://unpkg.com/[email protected]/dist/cdn.min.js" defer></script>
</head>
<body>
<div x-data="">
<span x-text="'Text literal'"></span>
</div>
</body>
</html>
Besides including the Alpine package via CDN (you can learn about the defer
semantics here), the only two Alpine-related things here are the directives x-data
and x-text
.
If you put this into an HTML page on your system and view it in the browser, you’ll see the message output, “Text literal.” While not terribly impressive, this application demonstrates two interesting facets of Alpine.
First, in order for the reactivity to engage, you must enclose the markup in an x-data
directive. If you remove the directive, the x-text
will not take effect. In essence, the x-data
directive creates an Alpine component. In this example, the x-data
directive is empty. In real use, you almost always have data in there; after all, you are writing components whose purpose is to be reactive to data.
The second thing to note in Listing 1 is that you can put any valid JavaScript into the x-text
. This is true of all the Alpine directives.
The x-data and x-text elements
The x-data
contents are provided to all contained elements. To understand what I mean, take a look at Listing 2.
Listing 2. x-data and x-text interaction
<div x-data=" message: 'When in the course of human events...' ">
<span x-text="message"></span>
</div>
Now the page will output the beginning of the Declaration of Independence. You can see that x-data
defines a plain old JavaScript object with a single field, 'message'
, which contains the Declaration’s preamble. The x-text
refers to this object field.
Reactivity in Alpine.js
Next, we’ll use reactivity to fix up an error in the Declaration. Take a look at Listing 3.
Listing 3. x-on:click and reactivity
<div x-data=" pronoun: 'men' ">
<button x-on:click="pronoun = 'people'">Fix It</button>
<span x-text="`all $pronoun are created equal`"></span>
</div>
The x-text
directive should be self-evident now. It refers to the pronoun
variable exposed by the x-data
directive. The new piece here is the button, which has an x-on:click
directive. The handler for this click event replaces the old default pronoun with a gender-neutral one, and reactivity takes care of updating the reference in the x-text.
Functions in data
The data properties in Alpine are full-featured JavaScript objects. Let’s consider another way to handle the above requirement, shown in Listing 4.
Listing 4. Using data functions
<div x-data="
pronoun: 'men',
fixIt: function()
this.pronoun = 'people';
">
<button x-on:click="fixIt()">Fix It</button>
<span x-text="`all $pronoun are created equal`"></span>
</div>
In Listing 4 you can see that the data object now hosts a fixIt
method that is called by the click handler.
As an aside, note that you will sometimes see application code that calls from the x-data
directive to a function defined in a script tag—this is a personal preference and it operates exactly the same as an inline x-data
:
<div x-data="myDataFunction()">...</div>
...
<script>
function myDataFunction()
return
foo: "bar"
</script>
Fetching remote data
Now let’s switch gears and think about a more complex requirement. Say we want to load a JSON-formatted list of the American presidents from an external API. The first thing we are going to do is load it when the page loads. For that, we’ll use the x-init
directive, as shown in Listing 5.
Listing 5. Preloading data from x-init
<div x-data="
presidents: []
"
x-init="(
async () =>
const response = await fetch('https://raw.githubusercontent.com/hitch17/sample-data/master/presidents.json');
presidents = await response.json();
)">
<span x-text="presidents"></span>
</div>
What’s happening here? Well, first of all, the x-data
directive should be clear: it simply has a presidents
field with an empty array. The x-text
in the span
element outputs the contents of this field.
The x-init
code is a bit more involved. First off, notice that it is wrapped in a self-executing function. This is because Alpine expects a function, not a function definition. (If you were to use the non-asynchronous callback form of fetch
, you wouldn’t need to wrap the function like this.)
Once we’ve obtained the list of presidents from the endpoint, we stick it into the presidents
variable, which Alpine has exposed as part of the x-data
object.
To reiterate: Alpine.js is making the data from a-data
available to the other directive functions (like x-init
) within the same context.
Iterating with Alpine.js
At this point, our application is pulling the data from the remote endpoint and saving it into the state. Note, however, that it is outputting something like [Object],[Object]...
. That is not what we want. Let’s get a look at iterating over the data, as shown in Listing 6.
Listing 6. Iterating with Alpine.js
<div x-data=...>
<ul>
<template x-for="pres in presidents">
<li><div x-text="pres.president"></div>
From: <span x-text="pres.took_office"></span> Until: <span x-text="pres.left_office"></span></li>
</template>
</ul>
</div>
Listing 6 contains a normal unordered list followed by an HTML template element, which contains an x-for
directive. This directive operates similarly to what you may have seen in other reactive frameworks. In this case, it allows us to specify a collection, presidents
, and an identifier that will be provided to the enclosed markup representing each instance of that collection, in this case, pres
.
The rest of the markup uses the pres
variable to output data from the objects via x-text
.
The application now looks something like what is shown in Figure 1.
Show/hide and onClick
Now we want to set up the application so that the data for the president is toggled by clicking on the president’s name. To start, we modify the markup to what is shown in Listing 7.
Listing 7. Show/Hide elements
<template x-for="pres in presidents">
<li><div x-text="pres.president" x-on:click="pres.show = ! pres.show"></div>
<div x-show="pres.show">
From: <span x-text="pres.took_office"></span> Until: <span x-text="pres.left_office"></span></li>
</div>
</template>
Now, in Listing 7, we can use the x-show
directive on a div
containing the presidential details. The truthiness of the x-show
value determines if the content is visible. In our case, that is determined by the pres.show
field. (Note that in a real application, you might not want to use the actual business data to host the show/hide variable.)
To change the value of pres.show
we add an x-on:click
handler to the header. This handler simply swaps the true/false value of pres.show
: pres.show = ! pres.show
.
Add transition animation
Alpine includes built-in transitions that you can apply to the show/hide feature. Listing 8 shows how to add the default animation.
Listing 8. Add a transition to show/hide
<div x-show="pres.show" x-transition>
From: <span x-text="pres.took_office"></span> Until: <span x-text="pres.left_office"></span></li>
</div>
The only thing that changed is that the element bearing the x-show
directive now also has a x-transition
directive. By default, Alpine applies sensible transitions. In this case, the transition is a slide-and-fade effect. You can customize the transition extensively, including by applying your own CSS classes to various stages of the animation. See the Alpine.js transition docs for more about this feature.
Binding to inputs
Now, we’ll add a simple filter capability. This will require adding an input that you bind to your data, and then filtering the returned dataset based on that value. You can see the changes in Listing 9.
Listing 9. Filtering the presidents
<div x-data="
filter: '',
presidents: [],
getPresidents: function()
return this.presidents.filter(pres => pres.president.includes(this.filter) )
"
...
<input x-model="filter" />
...
<ul>
<template x-for="pres in getPresidents">
Notice that the x-data
object now has a “filter” field on it. This is two-way bound to the input element via the x-model
directive which points to “filter
.”
We’ve changed the template x-for
directive to reference a new getPresidents()
method, which is implemented on the x-data
object. This method uses standard JavaScript syntax to filter the presidents based on whether they include the text in the filter field.
Conclusion
Like its namesake, Alpine.js is a lightweight backpack loaded with the basic gear to get you through the mountains. It is minimal, but sufficient.
The framework includes some higher-level features, notably a central store and an eventing system, as well as a plugin architecture and ecosystem.
In all, Alpine.js is ergonomic to work with. If you have experience with other reactive frameworks, Alpine should be familiar enough that you’ll quickly pick it up. The simplicity of declaring a component and its data in an x-data
directive smacks of genius.
You might wonder about intercomponent communication. Alpine.js eschews explicit wiring between components (no parent-to-child props, for instance). Instead, it uses the browser environment (that is, the window) as an event bus via the $dispatch
directive. This is in line with Alpine’s philosophy of adding just enough functionality to augment what’s already there. It works well.
All of these elements are put to the test as an application grows in size and complexity. So it goes with any stack you choose. Alpine.js is a tempting option for the next time you go code venturing.
Copyright © 2022 IDG Communications, Inc.