Intro to Alpine.js: A JavaScript framework for minimalists

Alpine.js is a front-end JavaScript framework fashioned like a lightweight backpack, with a minimalist API and thoughtful features. Let's give it a try.

mountains beneath a binary sky
Etienne Bösiger / Gerd Altmann (CC0)

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/alpinejs@3.10.3/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.

A list of United States presidents generated with Alpine.js. IDG

Figure 1. A list of the United States presidents.

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.