Getting Started with Alpine.js - The Ultimate Guide

Getting Started with Alpine.js - The Ultimate Guide
Author
Chidume Nnamdi
Related tags on daily.dev

🎯

Many things have been written about Alpine.js and here we decided to include them all. Check out this guide for anyone who want to start with Alpine.js.

Haven’t we had enough? Enough JavaScript frameworks and libraries. Almost every week, a new JavaScript framework or library is launched. Each of them comes with its uniqueness. Here, we have another JavaScript library, Alpine.js. It is quite similar to Vue.js and jQuery but it packs its powerful punch. We will learn about them soon enough.

What will be covered in this guide?

1. What is Alpine.js?

2. How good is Alpine? What is its super-power?

  • What is the Alpine.js file structure?
  • Is this framework for me?
  • What is the difference between jQuery and Alpine.js?
  • Can we use this framework like jQuery?
  • Are they using this in TailwindCSS?

3. How can Alpine.js come in lighter despite its API being equivalent Vue.js?

4. Tutorial

  • Installation
  • x-data
  • x-init
  • x-show
  • x-bind
  • x-bind class
  • x-bind boolean
  • x-text
  • x-html
  • x-if
  • x-for
  • x-on
  • x-on modifiers
  • x-model
  • x-model modifiers
  • x-ref

5. Magic properties

  • $el
  • $refs
  • $event
  • $nextTick
  • $dispatch
  • $watch

6. How to "watch" a component property

  • Build Movie search application using Alpine.js
  • Demo

7. Conclusion

8. References

What is Alpine.js?

Alpine.js is a JavaScript framework that enables us to enhance our JavaScript applications with its reactive and declarative nature. It is not used to create SPAs, it just uses the nature of Angular.js to strengthen your templates.

The main power of Alpine.js is its reactive nature. You use its directives to bind data and any changes made to it reverberates in the whole app regardless of the level of the emanation. Alpine.js is more like a UI interaction framework. If you have used Vue.js or Angular.js especially Alpine.js will be easy to wrap your head around because it is the same syntax as Angular.js.

Alpine.js is also a DOM manipulation library. It makes it easy for us to interact with the DOM in a declarative way with lesser code. Much less than what we can achieve using vanilla JavaScript or jQuery. The same thing can be done with frameworks React, Angular, but the size of these frameworks bloats up our app and makes it overkill.

Let’s see some statements about Alpine.js from its creators and other devs:

A rugged, minimal framework for composing JavaScript behavior in your markup. - alpinejs/alpine
Alpine.js offers you the reactive and declarative nature of big frameworks like Vue or React at a much lower cost. You get to keep your DOM and sprinkle in behavior as you see fit. - Alpine.js docs on the Github repo README.md
Alpine.js is for developers who aren’t looking to build a single page application (SPA). It’s lightweight (~7kB gzipped) and designed to write markup-driven client-side JavaScript. — Hugo Di Francesco

How good is Alpine? What is its super-power?

Binding and Querying: This is the greatest strength of Alpine.js. We use the `x-data`, `x-show` directives to bind and query data in Alpine.js, these directives use reactivity to bind data and reflect any changes made to the data at any level in the app.

Events: Events such as click, mouse, key events can be created in Alpine.js, and it handles them perfectly. Also, custom events can be created and triggered.

Size: Alpine.js has the power of the big frameworks, and yet with a very small bundle size 7.1kB gzipped (21.9kB minified). jQuery 32.5kB gzipped (92.4kB minified ); Vue.js 22.8kB gzipped (63.5kB minified); React 5kb gzipped; React-Dom 39.4kB gzipped (121.1kB minified); React+React-Dom 31.8kB gzipped (133kB minified).

What is the Alpine.js file structure?

Alpine.js has no file structure also, it doesn’t currently have a CLI. We can expect that in the future, where they can give us a CLI tool we can use to scaffold a quick Alpine.js project. Just like Vue.js CLI does.

For now, we manually create the files and templates and add the Alpine.js library. Since Alpine.js and Vue.js are similar in syntax, we can mimic the Vue.js file structure in our Alpine.js projects.

Is this framework for me?

I would say, yes, this framework is definitely for you. If you are coming from the Angular.js, jQuery, or Vue.js world you would see this framework works the same as them, and very super reactive.

Secondly, Alpine.js unlike other frameworks requires no build, simply include the library and off you go. The syntax being familiar to Angular.js, Vue.js and jQuery is a huge advantage too.

What is the difference between jQuery and Alpine.js?

Both Alpine.js and jQuery provide APIs for DOM manipulation. Alpine.js is declarative in nature and also reactive, while jQuery is non-reactive. Alpine.js uses reactivity for attribute binding, variable(data) binding, and event listeners. Everything in Alpine.js is reactive. jQuery is heavier than Alpine.js in terms of bundle size. jQuery is 32.5kB gzipped while Alpine.js is 7.1kB gzipped. jQuery is 92.4kB minified while Alpine.js is 21.1kB. We see here that Alpine.js comes out lighter than jQuery.

Due to their bundle sizes, they have different download times. Alpine.js download time in 2G Edge and 3G is 236ms and 141ms respectively. jQuery download times on 2G and 3G are 1.01s and 0.61s respectively. We see that Alpine.js is smaller than jQuery and loads faster than jQuery.

Can we use this framework like jQuery?

Alpine.js is a framework that simplifies the DOM manipulations in our project, so it cant be used like jQuery. Both Alpine.js and jQuery do the same thing, but Alpine.js works on the HTML template while jQuery works from JavaScript. Let’s see the same app built on Alpine.js and jQuery. This was done by Alex Justesen.

Alpine.js version:


<div class="" x-data="{ count: 0 }" x-init="count = $refs.countme.value.length">
  <textarea
    name="body"
    class="char-limiter"
    rows="3"
    maxlength="280"
    x-ref="countme"
    x-on:keyup="count = $refs.countme.value.length"
  ></textarea>

  <div class="">
    <span x-html="count"></span> /
    <span x-html="$refs.countme.maxLength"></span>
  </div>
</div>

jQuery version:


<div class="">
  <textarea
    name="body"
    class="char-limiter"
    rows="3"
    maxlength="280"
  ></textarea>

  <div id="the-count" class="">
    <span id="current"></span> / <span id="maximum"></span>
  </div>
</div>

var charLimiter = function () {
  var element = $(".char-limiter");
  var max_length = $(element).attr("maxlength");

  $("#current").text(0);
  $("#maximum").text(max_length);

  $(element).bind("change keyup input", function () {
    var characterCount = $(this).val().length,
      current = $("#current"),
      maximum = $("#maximum"),
      theCount = $("#the-count");

    current.text(characterCount);
  });
};

$(document).ready(function () {
  charLimiter();
});

This app listens to keyup change input events and updates the number of characters typed whenever any of the events fires. We can see in the Alpine.js version that everything is done in the HTML template, all JavaScript code are inline expressions. In the jQuery version, APIs from the library are used to query the DOM and update the HTML. The HTML is written, then we manipulate the DOM from JavaScript using jQuery methods We see that we can’t use Alpine.js like jQuery.

Are they using this in TailwindCSS?

Not really, but Alpine.js and TailwindCSS share common techniques. Alpine.js was inspired by TailwindCSS, in fact, it was previously called “Tailwind for JavaScript”. Alpine.js works like TailwindCSS, TailwindCSS does not need CSS files to write out your styles. It is already packed with utility classes that can be composed to build any design, directly in your markup.

Example:


<div>
  <dt class="sr-only">Title</dt>
  <dd class="group-hover:text-white leading-6 font-medium text-black">
    {item.title}
  </dd>
</div>

So, Alpine.js took a cue from this and made it in such a way that it provides utility directives that we can use to compose JS apps without the need to work on JavaScript templates.

How can Alpine.js come in lighter despite its API being equivalent Vue.js?

Alpine.js and Vue.js APIs are equivalent. For example, we can bind an attribute to an expression in Alpine.js by doing this x-bind:attribute="express", we can set data variable in Alpine.js by doing this x-data="express". In Vue.js it is almost the same just that we replace x- with v-. Attribute binding:v-bind:attribute="express" Data binding: v-data="express".

So, despite all that Alpine.js is much lighter because it is declarative and leverages reactivity in watching data changes. So much code was shaved off which made it lighter.

Installation

Alpine.js is easy to start using. Let's go on a quick tutorial:

CDN

Alpine.js CDN link:


<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js" defer></script>

This is to be added at the end of your <head> section. To avoid unexpected behaviors from Alpine.js versions. it’s recommended to pin a specific version number in the CDN link:


<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.7.0/dist/alpine.min.js" defer></script>

NPM

Alpine.js can be installed from NPM. To install the library run the below command:


npm i alpinejs

Next, include it in your script:


import 'alpinejs'

Alpine.js has 14 built-in directives. In the below sections, we will learn about the basic directives.

x-data

Syntax:


<div x-data="[JSON data object]">...</div>

This directive is used to declare data in a component scope. The data is an object literal with its properties available to the children elements to the element it was declared. Example:


    <div x-data="{ name: 'chidume', age: 28 }">

        <div>Name: <b x-text="name"></b></div>

        <div>Age: <b x-text="age"></b></div>

    </div>

Here, we have data declared in the outer div element. The object literal in the x-data directive creates a data in the div element scope. It has properties `name` and `age` with values ‘chidume’ and 28. Inside the div element scope, we can refer to the properties to get their values. See, we displayed the name and age in the object literal in the `b` element inside the `div` element.

The output in the DOM will be this:


    <div>

        <div>Name: <b>chidume</b></div>

        <div>Age: <b>28</b></div>

    </div>

The x-data value cannot be accessed from elements outside the element it was declared. That is to say, only children of an element can access the x-data value declared on the element. Sibling elements, adjacent elements cannot access it. Another example:


<div x-data="{ name: 'chidume', nickname: 'ND', age: 28 }">

  <div>Name: <b x-text="name"></b></div>

  <div>Age: <b x-text="age"></b></div>

</div>

<div>

  <div>Nickname: <b x-text="nickname"></b></div>

</div>

What did we just do here? We added a `nickname` property to the component scope. Then, we tried to access it(`nickname`) outside the component scope `<div>Nickname: <b x-text="nickname"></b></div>`.

It will throw an error in the console because properties cannot be accessed outside its scope. We tried accessing it in an element adjacent to the div element it was declared. We can extract data into reusable functions. This means that we can create a function in the `script` section of our HTML file, the function will return an object literal. The object will contain properties that will be declared in the component scope it is called. Let's see an example:


<div x-data="init()">

  <div>Name:<span x-text="name"></span></div>

  <button @click="setName('nnamdi')">Set Name</button>

</div>


<script>

  function init() {

    return {

      name: "",

      setName: function (name) {

        this.name = name;

      },

    };

  }

</script>

See, the `init` function returns an object that has `name` and `setName` properties. These properties will be declared in the component scope of the element it is called. See in the `div` element, we called the `init` function in the `x-data` directive. Now, the properties `name` and `setName` are visible to all elements inside the `div` element. Alpine.js also supports destructuring objects to mix in multple objects. We can have two functions like this:


<script>

  function init() {

    return {

      name: "",

      setName: function (name) {

        this.name = name;

      },

    };

  }

  function initV2() {

    return {

      nickname: "",

      setNickname: function (nickname) {

        this.nickname = nickname;

      },

    };

  }

</script>

We have two functions now. The new `initV2` returns `nickname` and `setNickname`. We can use destructuring to mix in the two. Like this:


<div x-data="{...init(), ...initV2()}">

  <div>Name:<span x-text="name"></span></div>

  <button @click="setName('nnamdi')">Set Name</button>

</div>

See we created an object, then called the two functions destructing the contents using the triple dots `...`. It works!!

x-init

Syntax:


<div x-data="..." x-init="[expression]"></div>

According to Alpine.js README, x-init directive runs an expression when a component is initialized. This directive is just like the ngOnInit lifecycle hook in angular or the componentWillMount in React. Here is where our initialization logic runs. Example:


    <script>

        function setOpen() {

            this.open = false

        }

    </script>

    <div x-data="{ open: true }" x-init="setOpen()">

        <div>Open?: <span x-text="open"></span></div>

    </div>

Here, the x-init runs the setOpen() function when the div element is initialized or created. The function sets the open property in the x-data object literal in the div element to false. This makes the open data to be always false on the creation of the div element.

We can also run code after the component has mounted and initial updates have been made to the DOM. This is done by returning a callback in the `x-init` directive. This callback will be run after Alpine.js has made the first updates to the DOM. Example:


<div x-init="() => { init(); }">...</div>

The code inside the callback is our post-initialization code.

x-show

Syntax:


<div x-show="[expression]"></div>

This directive toggles the visibility of components. It works by manipulating the CSS style display property of the element. The directive takes an expression:


    x-show="[expression]"

The expression must resolve to a boolean value (true/false). If true the element is shown by setting its CSS display property to a value other than none. If false, the display property of the element is set to none. Example:


    <script>

        function show() {

            return true

        }

    </script>

    <div>

        <div x-show="show()">Shown</div>

        <div x-show="false">Not Shown</div>

    </div>

The first inner div, will be shown because the call expression in its x-show directive will return true. The second inner div, will not be shown because the Boolean expression resolves to false.

x-bind

Syntax:


<div x-bind:[attribute]="[expression]">...</div>

This directive is used to set the value of an element’s attribute. The JavaScript expression is evaluated and the result of the evaluation becomes the value of the specified attribute. Whenever the dependencies of the x-bind's expression change, the expression will re-run so the attribute's value reflects the new changes. Example:


    <div x-data="{ val: true }">

        <input x-bind:disabled="val" type="text" />

    </div>

We have an input box, the x-bind:disabled binds the value of the input box's disabled property to val, a data in the parent div's scope. So the input box will be disabled.


    <div>

        <input disabled="true" type="text" />

    </div>

Now, if we change the value of the val data to false. The input box's disabled property value will be false. You see? the expression’s dependencies are listened for changes, when any (dependencies values changes) occur, the expression is reevaluated and the bound attribute’s value is updated. Alpine.js provides x-bind attribute for binding class, boolean, value attributes. Let's see them below:

x-bind class

This is used to bind the class attributes to an element. The value of this is an object expression whose key properties are the names of classes to be bound. The values are boolean values, if true means the class will be applied, if not true, the class will not be applied. Example:

We have a CSS class:


.hidden {  display: none !important; }

Our Alpine.js template is this:


<div x-data="{'show': true}">  <div x-bind:class="{ 'hidden': show }">...</div> </div>

See that to bind class attributes we use x-bind:class. We first created a CSS class .hidden that hides any element it is applied to using the class attribute. In the Alpine.js html template, we first declared data that has a boolean show set to true. We now bind the data show to the hidden class attribute. Now, the div element will have the hidden in its class attribute if the show data is true, if not the hidden will be absent. This way we can toggle the visibility of the div element.

x-bind boolean

Boolean values can be passed to the x-bind directive. Some HTML attributes are passed with a boolean value. We can bind that attribute to a boolean value in Alpine.js. Example, see this template:


<div>  <video x-bind:autoplay="true" /> </div>

The autoplay attribute of the video element is set to true. the outout will be this:


<div>  <video autoplay="true" /> </div>

According to Alpine.js GitHub repo:

Boolean attributes are supported as per the HTML specification, for example `disabled`, `readonly`, `required`, `checked`, `hidden`, `selected`, `open`, etc.

x-text

Syntax:


<div x-text="[expression]"></div>

This directive is used to set the inner text node of an element. The directive uses the innerText DOM property on the element to achieve this. Example:


    <div x-data="{ text: 'I am Chidume' }">

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

    </div>

In the span element, its text will be the value of the text property declared in the div's data.


    <div>

        <span>I am Chidume</span>

    </div>

Whenever the text data changes, the text node of the span element will be updated to reflect the new changes.

x-html

Syntax:


<div x-html="[expression]"></div>

This directive sets the inner HTML of an element. This is just like using the DOM innerHTML property to set the contents of an element. Example:


    <div x-data="{ html: '<i>I am Chidume</i>' }">

        <span x-html="html"></span>

    </div>

The html property contains an HTML string, and we want it to be the HTML content of the span element. The `x-html` directive set on the span element will set the HTML string in the html property to be the inner HTML of the span element. The output in the DOM will be this:


    <div>

        <span><i>I am Chidume</i></span>

    </div>

If the HTML property changes, the inner HTML of the span element will update to the value of the change.

x-if

Syntax:


<template x-if="[expression]"><div>...</div></template>

This directive conditionally renders elements based on the condition of its expression. If the condition is true, the element is visible. If the condition is false, the element is not visible. x-if does not use the display property for its conditional rendering, it removes or adds the elements to the DOM. Alpine.js says it is best to use the x-if directive in <template></template> tags, this is because Alpine.js uses the real DOM, not a virtual DOM.


    <div x-data="{ val: true }">

        <template x-if="val">

            <div>Renders on x-if directive.</div>

        </template>

    </div>

The template must have a single root element inside it. See, the parent div has boolean data declared. Next, we used x-if. If the `val` value is true, then the inner div element will be added to the DOM to be shown. If the `val` is false the inner div will not be added to the DOM so it will not be shown. After initialization and rendering, if the value of the `val` data changes to false, the inner div will be removed from the DOM. If the `val` changes to true, the inner div will be added to the DOM.

x-for

Syntax:


<template x-for="[data] in [data]"><div>...</div></template>

This directive is just like the ng-repeat in AngularJS. It is used to iterate over the items in an array and create new DOM nodes for each item. Just like `x-if`, `x-for` is used in the template tag. Example:


    <div x-data="{ fruits: ['orange', 'mango', 'lime' ] }">

        <h3>List of fruits</h3>

        <template x-for="fruit in fruits">

            <div x-text="fruit"></div>

        </template>

    </div>

We have a fruits array declared in the parent div element. Next, we use the x-for directive to iterate over the fruits array. The fruit in the "fruit in fruits" expression, is a variable that holds the current value in the iteration. The output will be:


    <div>

        <h3>List of fruits</h3>

        <div>orange</div>

        <div>mango</div>

        <div>lime</div>

    </div>

x-on

Syntax:


<div x-on:[event]="[expression]"></div>

This directive attaches event listeners to the element it is set upon. When the event is emitted the JavaScript expression in the directive is run. Example:


    <script>

        function clickHandler(e) {

            console.log("Clicked")

        }

    </script>

    <div>

        <button x-on:click="clickHandler">Click</button>

    </div>

To attach an event to an element using the x-on directive, the event name is set to the x-on attribute with a `:` separating them. The above set a click event to the button, the expression in the `x-on:click` is a function statement. When the button is clicked the function `clickHandler` is run. We can also call the function:


    <script>

        function clickHandler(e) {

            console.log("Clicked")

        }

    </script>

    <div>

        <button x-on:click="clickHandler($event)">Click</button>

    </div>

This will work. We can remove the `x-on:` when attaching events to elements and replace them with `@`. This is a shorter way of attaching events to an element.

Previous:


<button x-on:click="search">Search</button>

Shorter form:


<button @click="search">Search</button>

The shorter form works as well as the conventional type.


<input x-on:input="search" />

Shorter form:


<input @input="search" />

x-on modifiers

The `x-on` directive has modifiers that add extra functionalities to it. `.keydown` modifier: This listens to key events only. They mostly used in input elements. We can specify the key we want to listen for. Example:


<div>

  <input x-on:keydown.escape="alert('Esc key  pressed.')" />

</div>

This listens for when the `esc` key is pressed, and when it does the handler will display a system alert with the text `Esc key pressed.`. Paste this in your project and test. Another example:


<div>

  <input x-on:keydown.enter="alert('Enter key pressed.')" />

</div>

This listens for the `Enter` key. Another example:


<div>

  <input x-on:keydown.arrow-up="alert('Up arrow key pressed.')" />

</div>

This listens for the up arrow key.


<div>

  <input x-on:keydown.arrow-down="alert('Down arrow key pressed.')" />

</div>

This listens for when the down arrow key is pressed.

x-model

Syntax:


<div x-model="[data item]"></div>

This directive is used for two-way binding. Most especially used in the input elements. What is two-way binding? A bound variable in the HTML view can be changed from the script and also from the HTML view. From either side the change occurred, both the script side and HTML view bound-data are updated. Example:


    <div x-data="{ val: 'Nnamdi' }">

        <input type="text" x-model="val" />

    </div>

The value of the input box is two-way data-bound to the `val` data. When we type on the input box, the `val` value will be changed to the text typed in. Also, when the value of the `val` is changed from the script side, the value of the input box is updated to the value of the `val`.

x-model modifiers

`x-model` modifiers are properties introduced by Alpine.js to add more functionality to the `x-model` directive. `.number` modifier when applied to an input converts the input's value to a number. Example:


<div x-data="{ q: '' }">

  <input x-model.number="q" />

</div>

See here, the `x-model.number` will convert the value of the input box to a number. Another one is the `.debounce` modifier, this is awesome. This helps us easily add debouncing to our code. This modifier makes the handler it is attached, not run until a certain amount of time has passed. That is, the handler will wait for a certain amount of time before it runs. Example:


<div x-data="{ q: '' }">

  <input x-model.debounce="q" />

</div>

Now, when we type in the input box, the value of the input box will be assigned to the `q` property. Since we added the `.debounce` modifier, the property `q` will not take the value input when typed into, it will wait 250ms before being assigned. So if we try to access the `q` before the time elaspes we will get the old value of the `q` property.

Note: 250ms is the default time for `.debounce` modifier set by Alpine.js.

We can set the time ourselves.


<div x-data="{ q: '' }">

  <input x-model.debounce.500ms="q" />

</div>

Here, it will take 500ms to pass before the value of the input box to be assigned to the `q` property.


<div x-data="{ q: '' }">

  <input x-model.debounce.1000="q" />

</div>

Here, we did not specify the unit of the time as we did in other examples. The default unit of time is `ms`. So, here 1000ms will elapse before `q` will be updated with the value of the input box.

x-ref

This directive is used to access and retrieve the DOM of elements it is applied to. To set `x-ref` in an element, we do this:


<div x-ref="title"><span>Title</span></div>

<div x-ref="year"><span>Year</span></div>

Now, we set the `x-ref` directive to two elements. The value of the `x-ref` directive will be set to `$refs` object by Alpine.js. From the `$refs` object, we can reference the element we want to access its DOM by the value in its `x-ref` directive. For example, to access the DOM of the first `div` element, we do this `$refs.title.innerText`. That gives us the inner text. To access the second element, it will be this `$refs.year.innerText`.

Magic properties

These properties have a magical effect on our Alpine.js project. They are found as global variables in our project and built-in. We will see them in the below sections.

$el

This magic property accesses and retrieves the DOM of the root component scope.


$el.innerHTML

This accessed the HTML string of the root component.


$el.innerHTML = "<div>Element</div>"

This resets the HTML of the root component to `"<div>Element</div>"`.

$refs

Holds the DOM reference of elements that has the `x-ref` directive on them. Example:


<div x-ref="title">Title</div>

The `title` will be in the `$refs` property. So we can get the HTML string of the `div` like this:


<button @click="alert($refs.title.innerHTML)"></button>

We used `title`, the name of the `x-ref` on the `div` element to get it from the `$refs` object, the returned value contains properties we can use to access the DOM of the element. Here, we just display the inner HTML string.

$event

This is used to access browser's native `Event` object. This is just like the default `e` we get in event handlers in normal JS apps.


function clickHandler(e) {

  //...

}

The `e` argument in the `clickHandler` is the same `Event` object we get in Alpine.js `$event`. Se, `$event` can be used like this:


<button @click="aFunction($event)">Search</button>

<script>

  function aFunction(e) {

    //...

  }

</script>

See we are expecting the `Event` object in the `aFunction`, in that case, `aFunction` is called in the expression and passed the `$event` magic property. The `e` param in the `aFunction` will recieve the `Event` object when the button is clicked. Another example:


<div>

  <input x-on:input="aFunction($event)" />

</div>

<script>

  function aFunction(e) {

    //...

  }

</script>

We registered an `input` event in the input box, and we have a call expression that calls the `aFunction` passing in the `$event` property. The `e` param in the `aFunction` will hold the `Event` when the function is called.

$nextTick

This magic property that is passed a callback function, this callback is only called when Alpine.js has made its updates.

`$nextTick` ensures expressions are only executed once Alpine has done its thing - Intro to Alpinejs - Smashing Magazine

Example:


<div x-data="{ text: 'Click me' }">

  <button

    class="btn btn-primary"

    @click="

            text = 'Clicked';

            $nextTick(() => {

                console.log($event.target.innerText) 

            });"

    x-text="text"

  ></button>

</div>

See the above code, we have a `text` property on our scope with an initial value of `Click me`. The text of the button is set by the value of the `text` property using the `x-text` directive. In the button we have a `click` event attached to it. The handler when clicked, sets the value of the `text` property to "Clicked" and calls the `$nextTick` magic property. Now, the callback in the `$nextTick` logs the text of the button. When the button is clicked, the callback in the `$nexttick` is never called until the DOM update made by `text = 'Clicked'` is done. That's why we will see "Clicked" as the text of the button instead of "Click me".

$dispatch

this magic property is used to dispatch, trigger or fire custom events in Alpine.js. It is called like this:


$dispatch('custome-event', payload)

The first param is the name of the custom event to fire, and the second param is the payload that will be passed to the handler function of the custom event. Example:


<div @c-event="console.log('I am a custom event')">

    <button @click="$dispatch('c-event', 90)">

</div>

In this example, we set a custom event on the `div` by doing this `@c-event`, this becomes an event with a handler that logs a text when it is fired. To fire this event, we used the `$dispatch` magic property, passing in the event name `c-event` and the event handler args. To access the event handler args, we use the `$event.details` object. It contains the args sent by the `$dispatch` property. Example:


<div @c-event="console.log($event.detail)">

    <button @click="$dispatch('c-event', 90)">

</div>

We can see that the `$dispatch` property does the same thing a creating a custom event using the `Event` object in HTML/JS and dispatching via `.dispatchEvent` method in the DOM. All this while we have been accessing the `$dispatch` via the Alpine.js expressions. We can access it in JavaScript by passing it as arg to a function call expression.


<div @c-event="console.log('Custom event called.')">

  <button x-on:click="myFunction($dispatch)">Search</button>

</div>

<script>

  function myFuction(dispatch) {

    dispatch("c-event", 90);

  }

</script>

See how we passed the `$dispatch` magic property to `myFunction`. Now, with the `dispatch` param will hold `$dispatch`, and we can then call it to dispatch events.

$watch

This magic method is used to "watch" or "listen" for changes in a component property. See the syntax here:


$watch('COMPONENT_PROP', (value) => {...})

The first param is the property in the component scope we want to "watch", the second param is the callback function that will be called whenever the value of the "watched" component property changes. Let's see an example from Alpine.js GitHub READMe.md:


<div

  x-data="{ open: false }"

  x-init="$watch('open', value => console.log(value))"

>

  <button @click="open = ! open">Toggle Open</button>

</div>

We have a `open` property in the component scope in the parent `div` element. In the `x-init` directive, we called the `$watch` directive to "watch" the `open` property by passing it as the first arg. The callback function has a `value` param that takes the value of the `open` property when it changes. The callback merely logs the new value of the `open` property. Clicking on the `Toggle Open` will change the value of the `open` and the callback function int he `$watch` magic method will be called, then, we will see the logs on our console.

How to "watch" a component property

Alphie.js uses the x-data directive to declare a new component scope. To "watch" a property in Alpine.js component scope, we use the x-model directive.

x-model adds "two-way data binding" to an element. Keeps input element in sync with component data.

Let's see an example:


<div x-data="{name:''}">  <label for="name">Name:</label>  <input id="name" type="text" x-model="name" />  <p x-text="name"></p> </div>

The x-data directive declares a new component scope in the div element. The component has a "name" property which is an empty string. The x-model is assigned to the name of the property to watch which is the name property in the component scope. x-model binds the name property to the input field so whenever the input field value changes the "name" property changes accordingly and the UI that reflects the property reflects the changes too. So in this way, we can "watch" the property of a component in Alpine.js.

Build movie search application using Alpine.js

To bring together what we have learned so far we will build a Movie search app. We will show you how to use the majority of Alpine.js directives. First, create a folder movie-search and create the files: index.html, styles.css:


- movie-search
    - index.html
    - styles.css

Open the index.html file and paste the following:


<html>
  <head>
    <script
      src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.8.1/dist/alpine.min.js"
      defer
    ></script>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body></body>
</html>

Here, the Alpine.js library is pulled from CDN. Also, we add our external stylesheet styles.css. Our movie search app will have a header, a search area, and a container where the movies will be displayed. Let’s flesh our HTML out:


<html>
  <head>
    <script
      src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.8.1/dist/alpine.min.js"
      defer
    ></script>
    <link rel="stylesheet" href="styles.css" />
  </head>

  <body>
    <div class="main" x-data="init()">
      <h4 class="font-xxlarge">Movie search in Alpine.js</h4>
      <div class="searchArea">
        <input
          class="inputText"
          type="text"
          placeholder="Type to search a fact"
          x-model="q"
        />
        <button class="bg-default" @click="search()">Search</button>
      </div>
      <div>
        <template x-for="result in results">
          <div class="movieCard">
            <div>
              <img x-bind:src="result.Poster" />
            </div>
            <div>
              <div class="movieDetailItem">
                <span style="padding-right: 5px">Title:</span
                ><span><b x-text="result.Title">Man of Steel</b></span>
              </div>
              <div class="movieDetailItem">
                <span style="padding-right: 5px">Year:</span
                ><span><b x-text="result.Year">2008</b></span>
              </div>
            </div>
          </div>
        </template>
      </div>
    </div>
    <script>
      function init() {
        return {
          results: [],
          q: "",
          search: function () {
            fetch(
              "http://www.omdbapi.com/?&apikey=e1a73560&s=" +
                this.q +
                "&type=movie"
            )
              .then((response) => response.json())
              .then((response) => (this.results = response.Search))
              .catch((err) => console.log(err));
          },
        };
      }
    </script>
  </body>
</html>

This is the full code, see how small the code is for such an app. That’s the power of Alpine.js. Let’s describe what we have done here. First let’s look at the script section:


<script>
  function init() {
    return {
      results: [],
      q: "",
      search: function () {
        fetch(
          "http://www.omdbapi.com/?&apikey=e1a73560&s=" + this.q + "&type=movie"
        )
          .then((response) => response.json())
          .then((response) => (this.results = response.Search))
          .catch((err) => console.log(err));
      },
    };
  }
</script>

Here is our init function, it will be called when the page is loaded. See it returns an object that contains our app state and functions to be called in the app.

Since this app will fetch the movies from the oMDB API, we have to set a results array where the movies will be stored when fetched by the API. Also, the movies will be searched by what is typed in the input box, so a state q is set to hold the search value. The search function will be called when the Search button is clicked, the function will call the oMDB API with the search value q passed as a parameter to the API URL. The fetch method will call the API to get the list of movies. The response is in a Promise, we get it in the response param and we use it to set the results array from its response.Search property. Let’s go to the body section. There we have a div element that encloses all our app components and elements. So in this top-level element, we declare our root component scope. This is done by calling the init function in this div passing it to the x-data directive.


<div class="searchArea">
  <input
    class="inputText"
    type="text"
    placeholder="Type to search a fact"
    x-model="q"
  />
  <button class="bg-default" @click="search()">Search</button>
</div>

This is our search area. It has an input box where the movie to search will be typed. See we have a x-model directive set to q. The q will be updated by the input box value when text is being typed in. The Search button has a click event attached to it, the handler calls the search function in the script section.


<div>
  <template x-for="result in results">
    <div class="movieCard">
      <div>
        <img x-bind:src="result.Poster" />
      </div>
      <div>
        <div class="movieDetailItem">
          <span style="padding-right: 5px">Title:</span
          ><span><b x-text="result.Title"></b></span>
        </div>
        <div class="movieDetailItem">
          <span style="padding-right: 5px">Year:</span
          ><span><b x-text="result.Year"></b></span>
        </div>
      </div>
    </div>
  </template>
</div>

This section displays the results of the movie search. We used the x-for directive to display the result of the search stored in the results array. The template inside the x-for holds how each movie will be displayed. See also, we used x-text to display the movie title and year. We used x-bind:src to bind the src attribute of the image to the image URL of the movie, this makes the movie image to be rendered. Now, here comes the styling:


html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
    Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
  color: inherit;
  text-decoration: none;
}

* {
  box-sizing: border-box;
}

.main {
  padding: 10px 0;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.inputText {
  padding: 10px 7px;
  border-radius: 3px;
  margin-right: 2px;
  margin-left: 2px;
  /*width: 100%; */
  font-size: large;
}

button {
  padding: 10px 10px;
  border-radius: 3px;
  cursor: pointer;
  margin-right: 2px;
  margin-left: 2px;
  font-size: large;
}

.bg-default {
  background-color: rgba(70, 130, 236, 1);
  border: 1px solid rgba(28, 28, 49, 1);
  color: white;
}

.bg-danger {
  background-color: red;
  border: 1px solid rgba(28, 28, 49, 1);
  color: white;
}

.font-xxlarge {
  font-size: xx-large;
}

.inputButton {
  padding: 10px 10px;
  border-radius: 3px;
  background-color: rgba(70, 130, 236, 1);
  color: white;
  border: 1px solid rgba(28, 28, 49, 1);
  cursor: pointer;
  margin-right: 2px;
  margin-left: 2px;
  font-size: large;
}

.movieCard {
  padding: 4px;
  border-radius: 3px;
  box-shadow: 3px 0 0 3px rgba(28, 28, 49, 1);
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  transition: 0.3s;
  margin: 17px 1px;
}

.movieCard:hover {
  box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
}

.movieDetailItem {
  padding: 5px 2px;
}

Now our app is complete. See the link to the app in Stackblitz below. You can play with it over there.

Conclusion

We learned a great deal about Alpine.js. We started with the pros of using Alpine.js. Next, we learned how to install the Alpine.js lib from CDN and NPM. Then, we learned the basic Alpine.js directives, we learned each of them with examples for a demo on how they work. If you have any questions regarding this or anything I should add, correct or remove, feel free to comment, email, or DM me.

References

github.com

tailwindcss.com

vuejs.org

smashingmagazine.com

codewithhugo.com

Why not level up your reading with

Stay up-to-date with the latest developer news every time you open a new tab.

Read more