If you read this blog, you likely know I predominantly work with .NET technologies and use web technologies such as HTML, CSS, and JavaScript. The web is an excellent space for new and exciting ways to solve age-old problems. I recently thought about the Blazor “Counter” example that ships with the Blazor Wasm template and how I’ve solved the same problem using Htmx. The issue with Htmx is that it still requires a backend to manage the state; in the case of a counter, this state is the current count. What if you wanted to build a self-contained client-side experience?

Alpine.js is a declarative library aimed at helping developers build client-side interactivity using HTML attributes on DOM elements. This post will show you how to create the same Blazor Counter example with very little JavaScript and network payloads.

Installing Alpine.js

Since Alpine.js is a JavaScript library, you only need to reference the necessary files in your HTML pages. In an ASP.NET Core application, that’s typically in your layout files.

Add the following script tag to the head portion of your page.

<!-- Alpine Core -->  
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

That’s it. You can also use NPM to bundle the dependency into an existing build process, but we’ll leave that out of this post for now.

Building an inline Counter with Alpine

Alpine uses attributes, and one of the most important attributes is the x-data attribute. The x-data attribute sets up the context for our current scope. In most cases, that scope is the DOM element you decorate with the attribute.

<main class="container" x-data="{ count : 0 }">
    <article class="card">
        <header>
            <h3>Counter Value</h3>
        </header>
        <section>
            <p x-text="count"></p>
        </section>
    </article>

    <button type="button" x-on:click="count++">
        Increment
    </button>
    <button type="button" x-on:click="count = 0">
        Reset
    </button>
</main>

Wow, that’s easy, right? So, how do we persist this information on reloads? Local storage of course!

Making the inline counter persistent

Alpine.js has a persistence plugin, which we’ll need to install. While you can write the local storage code, this plugin makes it much nicer to use stored values and update them as users make changes.

Modify the reference to Alpine.js on the page to include these two script tags.

<!-- Alpine Plugins -->  
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>  
<!-- Alpine Core -->  
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

Cool, we’re ready to update the HTML to persist the value across page refreshes.

<main class="container" x-data="{ count : $persist(0) }">
    <article class="card">
        <header>
            <h3>Counter Value</h3>
        </header>
        <section>
            <p x-text="count"></p>
        </section>
    </article>

    <button type="button" x-on:click="count++">
        Increment
    </button>
    <button type="button" x-on:click="count = 0">
        Reset
    </button>
</main>

Refreshing the page now maintains the previous count. What if we want to use that value in other page parts? In other words, what if we want to keep a globally accessible value? That’s where Alpine.js stores come in.

Persisting state in an Alpine Store

Alpine.js allows you to create a global state using a store. Stores are created when Alpine.js initializes the page, allowing you to write more complex logic using JavaScript. Let’s modify our page to use a counter store with persistent values and methods.

<body>
    <main class="container" x-data>
        <article class="card">
            <header>
                <h3>Counter Value</h3>
            </header>
            <section>
                <p x-text="$store.counter.value"></p>
            </section>
        </article>
        
        <button type="button" x-on:click="$store.counter.increment()">
            Increment
        </button>
        <button type="button" x-on:click="$store.counter.reset()">
            Reset
        </button>             
    </main>
    <script>
        document.addEventListener('alpine:initializing', () => {
            Alpine.store('counter', {
                value: Alpine.$persist(0),
                increment() {
                    this.value++
                },
                reset() {
                    this.value = 0;
                }
            })
        })
    </script>
</body>

It’s a bit more JavaScript code, but now other page elements can access the counter store, and the concepts of increment and reset are incapsulated into the store, allowing us to change behavior in a single location. That’s pretty cool!

Here’s the full HTML file so you can try it out for yourself.

<!DOCTYPE html>
<html lang="en" xmlns:x-on="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>PicoCSS Boilerplate</title>
    <link
            rel="stylesheet"
            href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
    />
    <link rel="stylesheet" href="/css/site.css"/>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="color-scheme" content="light dark"/>
    <title>Hello World!</title>
    <!-- Alpine Plugins -->
    <script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
    <!-- Alpine Core -->
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body>
<main class="container" x-data>
    <article class="card">
        <header>
            <h3>Counter Value</h3>
        </header>
        <section>
            <p x-text="$store.counter.value"></p>
        </section>
    </article>

    <button type="button" x-on:click="$store.counter.increment()">
        Increment
    </button>
    <button type="button" x-on:click="$store.counter.reset()">
        Reset
    </button>
</main>
<script>
    document.addEventListener('alpine:initializing', () => {
        Alpine.store('counter', {
            value: Alpine.$persist(0),
            increment() {
                this.value++
            },
            reset() {
                this.value = 0;
            }
        })
    })
</script>
</body>
</html>

Conclusion

Alpine.js is an excellent library for building client-side experiences, and mixing it with Htmx or plain old JavaScript is a winning combination. I hope you try this and experiment with changing the behavior of increment and reset.

As always, thanks for reading and sharing my posts with others. Cheers.