Introduction to Alpine.js

In this post, we will introduce the Alpine.js library - this is a lightweight JavaScript framework for building interactive applications that circumvents the need for complex JavaScript tooling. It's directives are inspired by Vue.js, but Alpine.js has a more minimal set of functionality, making it a good option for building interactive apps with minimal JavaScript overhead.

We will work through some toy examples in this post, to demonstrate the key directives offered by Alpine.js. In later posts, we'll focus on integration with Django and integration with other tools such as HTMX.

The associated video for this post is shown below.


Objectives

In this post, we will:

  • Learn how to use Alpine.js directives in HTML code
  • Learn how to build small, interactive tools using Alpine.js
  • Learn how to setup Alpine.js in Django templates

Project Setup

To get started, clone this Django project starter code here. For the majority of this post, we will not use Django much, but this starter code exists to allow users to work through future Django-based posts.

Once cloned, you should install the requirements with the pip install -r requirements.txt command. If you only want to use Alpine.js, and are not concerned about backend functionality, then this step isn't strictly necessary.

This starter project has a base.html template, and an index.html template which extends this base. We are going to include Alpine.js via a <script> tag in our base.html template - within the <head> tag, add the following line:

<script defer src="https://unpkg.com/alpinejs@3.7.1/dist/cdn.min.js"></script>

This references the latest version of Alpine.js, at the time of writing. You can find more details on including Alpine, as well as the most recent versions, here.

Since our index.html file extends the base.html file, we now have access to Alpine.js directives in the index.html file. This file is where we will work throughout this tutorial.

Building a Counter with Alpine.js

Let's build a simple counter using Alpine. This will consist of a count that we will store in state, which can be incremented when the user clicks a button on the page. We will also show the current state of the counter within an HTML element. Add the following code to the index.html file.

<div x-data="{count: 0}">
    <button x-on:click="count++">Increment</button>

    <span x-text="count"></span>
</div>

This code includes 3 different Alpine directives, attached to the HTML elements:

  • x-data - the most important directive: this defines the HTML within it as an Alpine component, and provides reactive data for the child elements in the component to reference. In the above example, we define a single piece of state, the count, and set its initial value to 0.
  • x-on - this directive listens for DOM events, and allows you to run code when an event occurs. In the above example, we have a <button> element, and when this is clicked, we increment the count with the count++ JavaScript syntax. Notice that we can refer to fields in our x-data directives within the value of other Alpine directives.
  • x-text - this directive allows us to set the text content of the element to which it is attached. Here, we set the text of the <span> element to the current value of the count state.

The effect of adding this simple code can be seen below.

Whenever we click the button, the value of the counter increments, and this change is reflected within the text of the <span> element.

We can also add more complex changes to state. Another fairly simple example is to square the current value of the counter, which can be achieved with a new button, below:

<button x-on:click="count *= count">Square</button>

This time, when the button is clicked, the current value of the count is squared. The effect can be seen below:

We can see how easy it is to refer to keys stored in the x-data directive, and how easy it is to dynamically update their values on actions.

Before we move on, let's look at a more common and shorthand syntax for the x-on directive. Instead of specifying x-on:click, we can simply write @click. So we can replace the following two lines:

<button x-on:click="count++">Increment</button>
<button x-on:click="count *= count">Square</button>

With the following:

<button @click="count++">Increment</button>
<button @click="count *= count">Square</button>

This syntax comes directly from Vue.js, and applies to all any event type.

Finally, let's give users the ability to set the value of the counter directly. Change the code to add the following:

<div x-data="{ count: 0 }">
    <input type="number" x-model="count" />
    <button @click="count++">Increment</button>
    <button @click="count *= count">Square</button>
 
    <span x-text="count"></span>
</div>

The new code is on line 2, where we create an <input> element with an x-model="count" directive. This directive binds the value of the <input> element to the count key in our Alpine data, meaning that whenever the user types a number into this field, it will update the value of the count key.

This looks like the following:

The x-model directive is very useful and allows users to directly mutate the state of our Alpine data.

Let's now move onto another example.

Creating List of Users

This time, we're going to set some slightly more complex Alpine data, that represents a list of users. Each user will have a name, and will have an image associated with them. We will iterate over these users with an Alpine x-for directive, and will display the person's name on the webpage. Add the following code to the index.html template:

<div x-data="{
    people: [
        {name: 'Mark', image: 'https://via.placeholder.com/150'},
        {name: 'Jeff', image: 'https://via.placeholder.com/150'}
    ],
    showImages: false 
}">

    <template x-for="person in people">
        <div>
            <p x-text="person.name"></p>
        </div>
    </template>

</div>

The x-data directive defines two fields in our Alpine:

  • people - the array of JavaScript objects, one for each person, with attributes name and image
  • showImages - a boolean value, which is initially false

We also have, on line 9, a <template> tag with an x-for directive. This directive allows you to create HTML elements by iterating through a list/array - in our example, we create a paragraph tag for each user in the list, showing their name with the x-text="person.name" directive.

Note: as per the Alpine docs, when using the x-for directive, you must have a single root element, and the x-for directive must be declared on a <template> element.

Let's make this a bit more interesting, and conditionally render the images associated with each person, depending on whether the showImages boolean is true or not. Add the following code:

<div x-data="{
    people: [
        {name: 'Mark', image: 'https://via.placeholder.com/150'},
        {name: 'Jeff', image: 'https://via.placeholder.com/150'}
    ],
    showImages: false 
}">

    <template x-for="person in people">
        <div>
            <p x-text="person.name"></p>
            <img x-show="showImages" x-bind:src="person.image" />
        </div>
    </template>

</div>

The new code is on line 12, where we add an <img> element. To this we add two new Alpine.js directives:

  • x-show - this directive allows you to show and hide DOM elements based on whether values in your Alpine state are truthy or falsy.
  • x-bind - this directive allows you to set HTML attributes on elements based on the results of JavaScript expressions, and allows you to reference Alpine state/data in doing so.

We bind the src attribute of the <img> element to the value of the image key at each iteration over our list of people.

We can use shorthand syntax for the x-bind directive, and can simply use a colon, followed by the name of the attribute - for the above example, this would change the <img> element to the following:

<img x-show="showImages" :src="person.image" />

Currently, users would NOT be able to see the images, because the value of the showImages boolean is false, therefore the element will be hidden via the x-show directive above.

Let's now give users a mechanism to toggle whether or not they can see the images, with the following code:

<div x-data="{
    people: [
        {name: 'Mark', image: 'https://via.placeholder.com/150'},
        {name: 'Jeff', image: 'https://via.placeholder.com/150'}
    ],
    showImages: false 
}">

    <button @click="showImages = !showImages">Show Images</button>

    <template x-for="person in people">
        <div>
            <p x-text="person.name"></p>
            <img x-show="showImages" :src="person.image" />
            <hr />
        </div>
    </template>

</div>

The button on line 9 contains an @click directive - when clicked, it will invert the boolean value of the showImages state. For example, if this is false and the button is clicked, the value will be changed to true - and vice versa.

When the value changes, the images will be toggled based on the x-show directive on the <img> element (remember, this depends on the value of showImages). This has the following effect:

We can also fix the text that's displayed in the button to say "Hide Images" when the images are shown, and "Show Images" when the images are hidden. Change the code for the button to the following:

<button @click="showImages = !showImages"
        x-text="showImages ? 'Hide Images' : 'Show Images' "></button>

We use the ternary expression in the x-text directive to change the button's text depending on the value of the showImages boolean.

We can apply a similar method to change an element's class. Change the paragraph element displaying the person's name to the following:

<p x-text="person.name" :class="person.name.startsWith('M') ? 'text-danger' : 'text-success' "></p>

This contrived example will add a class of text-danger if the person's name begins with 'M'; otherwise, the text-success class will be applied. These are Bootstrap classes that change the colour of the text within an HTML element. So now, Mark's name will be coloured red, and Jeff's name will be green.

x-transition

We can also add transitions easily for toggling the visibility of an element with x-show on it. Simply add the x-transition directive - we can apply this to our <img> element (which has x-show):

<img x-show="showImages" :src="person.image" x-transition />

This will have the effect of creating a simple transition when displaying or hiding the images. We can customize the transition further, for example by adding a duration:

<img x-show="showImages" :src="person.image" x-transition.duration.1500ms />

This has the following effect:

Check out the Alpine documentation for more information on transitions, and building more complex animations.

x-init directive

Let's conclude this post by demonstrating how to fetch data from a server (i.e. our Django server), and bring that into your Alpine component. We can use the x-init directive to achieve this. More details on this directive can be found in the Alpine docs.

Rather than hard-coding the array of people, let's now fetch this data from the server. Firstly, we'll create a Django view called people:

def people(request):
    people = [
        {"name": 'Mark', "image": 'https://via.placeholder.com/150'},
        {"name": 'Jeff', "image": 'https://via.placeholder.com/150'}
    ]
    return JsonResponse(people, safe=False)

This view simply returns the same data that we previously hard-coded in the x-data attribute, using Django's JsonResponse. Now, add a Django URL for this view, in core/urls.py:

urlpatterns = [
    path('', views.index, name='index'),
    path('people/', views.people, name='people')
]

Let's now amend the index.html template to use the x-init directive. The code should look as follows:

<div x-data="{people: [], showImages: true}"
     x-init="people = await (await fetch('/people/')).json()">

<button @click="showImages = !showImages"
        x-text="showImages ? 'Hide Images' : 'Show Images' "></button>

    <template x-for="person in people">
        <div>
            <p x-text="person.name" 
                :class="person.name.startsWith('M') ? 'text-danger' : 'text-success' "></p>
            <img x-show="showImages" :src="person.image" x-transition.duration.1500ms />
        </div>
    </template>

</div>

Within our <div> element at the top, we have removed the hard-coded array of people - now, it's initial value is an empty array.

We have also added the x-init directive, and are using the Fetch API to send a GET request to the new Django URL/view we have just defined. We await the response, await its conversion to JSON, and use this JSON to populate our people array within x-data.

The data will now be populated by our backend Django view - this is the mechanism by which we would fetch real data from a database. We will investigate such techniques in a future post.

Summary

This video has covered the most important directives provided by Alpine.js. We have build a couple of fun, interactive components, and have seen how to achieve some common tasks using Alpine.js.

If you enjoyed this post, please subscribe to our YouTube channel and follow us on Twitter to keep up with our new content!

Please also consider buying us a coffee, to encourage us to create more posts and videos!

;