Lessons

ACF Blocks Powered with Alpine.JS

What is Alpine and how to use it in themes and blocks


Steps in this lesson

  1. What is Alpine.js
  2. Why Alpine.js?
  3. Installing Alpine.js
  4. Use cases for themes
  5. Use cases for blocks
  6. Alpine and ACF Inner Blocks

What is Alpine.js

Alpine.js is a seriously lightweight JavaScript framework. Alpine is often referred to as “Tailwind for your JavaScript”, because like utility-classes, you can compose behavior directly in your markup.

Consider this simple example of a button that toggles the visibility of the paragraph below it:

<div x-data="{ open: false }">
    <button @click="open = !open">Hide / Show</button>
 
    <p x-show="open">
      Now you see me...
    </p>
</div>

x-data is where we set the “state” of our component. We set a piece of state called “open” and we default it to false.

We have a click event: @click="open = !open" that can update the state of “open”. Clicking this button will toggle the value between true and false.

Finally, we have x-show which will show the element if “open” is true and it will hide it with display: none; if “open” is false.

Here’s that same code sample IRL:

Now you see me...

How cool is that? We have a functioning component with state, event handlers, and we didn’t have to leave our HTML or write a single line of JavaScript.

Why Alpine?

#RealTalk I think the “best” tool is the tool that let’s you do your best work. And for me that tool is Alpine.js. If you’re using jQuery still, I think you have a lot to gain by exploring other tools, but that’s were my opinion ends.

Alpine.js encourages patterns that has resulted in me writing less JavaScript. When I do write JavaScript (usually for larger components) the JavaScript that I write is leaner and cleaner.

If I’ve intrigued you enough to consider giving Alpine.js a try, read on!

Installing Alpine.js

There’s nothing better than following the docs, but here’s a brief walk through to get started.

You can either include Alpine via CDN:

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

Or you can install it via NPM and make it a part of your project’s repo.

  1. Install via npm
npm install alpinejs
  1. Import Alpine
    Where ever we want to include Alpine in our JavaScript.
import Alpine from 'alpinejs'
 
window.Alpine = Alpine
 
Alpine.start()
  1. Build your JavaScript
    You can use something simple like ES Build to watch your JavaScript for changes and bundle it:
esbuild input.js --bundle --outfile=output.js --watch
  1. Enqueue JavaScript
    You need to load this JavaScript into your WordPress theme. Update the paths to match your project.
/**
 * Enqueue scripts and styles.
 */
function my_theme_enqueue_scripts()
{
    wp_enqueue_script( 'alpine', get_stylesheet_directory_uri() . "/dist/js/alpine.min.js");
}
add_action( 'wp_enqueue_scripts', 'my_theme_enqueue_scripts' );
  1. Dequeuing jQuery
    For custom themes, we really don’t need to keep jQuery around. Especially since we’re intentionally replacing jQuery with Alpine.js. It’s possible a plugin will re-enqueue it, but with Alpine we certainly don’t need it.
if (!is_admin()) {
    /** 
    * Get the heck outta here.
    * No, seriously. Leave.
    **/
    wp_dequeue_script('jquery');
}

The ! is_admin() part ensures we’re only dequeueing jQuery if we are NOT in WP Admin - IOW, just the front-end.

Two ways to use Alpine

Whether we install Alpine via CDN or bundle it into our project, we can either:

  1. Sprinkle Alpine directly in our markup or
  2. Extract Alpine logic into traditional JS with Alpine.data

With Alpine.data, we can componentize our logic and reuse it.

<div x-data="notification">
    <input x-model="message" placeholder="Type your message">   
    <button @click="send()">Send it</button>
    <p x-text="message" x-show="visible"></p>
    <button @click="dismiss()">Dismiss</button>
</div>
import Alpine from 'alpinejs'
 
Alpine.data('notification', () => ({
    visible: false,
    message: "",

    send() {
        this.visible = true
    },

    dismiss() {
        this.visible = false
    }
}))

window.Alpine = Alpine
Alpine.start()

Give this notification component a try:

Use cases for Alpine.js in your theme

Mobile menu

If we had a jQuery mobile menu, our code might look like this:

<nav>
    <ul class="menu">
        <li><a href="/page-a">Page A</a></li>
        <li><a href="/page-b">Page B</a></li>
        <li><a href="/page-c">Page B</a></li>
    </ul>

    <button class="menu-button">Menu</button>
</nav>
$(document).ready(function(){
    $('.menu-button').click(function(){
      $('.menu').toggleClass('open');
    });
});
.menu {
    display: none;
}

.menu.open {
    display: block;
}

That doesn’t look too bad, but multiply that across multiple components (accordions, FAQs, modals, etc) and our markup, JavaScript, and CSS grows and grows just to toggle something’s visibility.

Let’s replace all of this with Alpine without leaving our markup:

<nav x-data="{open: false}">
    <ul x-show="open">
        <li><a href="/page-a">Page A</a></li>
        <li><a href="/page-b">Page B</a></li>
        <li><a href="/page-c">Page B</a></li>
    </ul>

    <button @click="open = !open">Menu</button>
</nav>

We were able to delete all of the JavaScript and CSS responsible for the visibility of the mobile menu.

Modals

This same process can be applied to modals:

<div x-data="{modalOpen: false}">
    <button @click="modalOpen = true">Login</button>
    ...
    <!-- Modal -->
    <div 
        x-show="modalOpen" 
        @click.away="modalOpen = false" 
        x-transition>
        <!-- Modal content -->
    </div>
</div>

There are a couple neat things happening here worth mentioning:

  1. @click.away - this click event watches for clicks outside of the modal. If you have a dark overlay behind the modal, your user can click it to dismiss it. We could do the same thing with @keyup.escape="modalOpen = false" if a user presses their esc key.
  2. x-transition - Alpine can apply default transition styles so that the modal subtly fades up and in. We can modify these transitions too.

Use cases for blocks

Accordion

Here’s a dead simple implementation for an accordion block. I’ll assume at this point you’re comfortable registering your own block and setting up ACF field groups.

<?php if( have_rows('accordion') ): ?>
    <?php while( have_rows('accordion') ) : the_row(); ?>
        <div
            class="accordion-item"
            x-data="{open: false}"> 

            <button @click="open = !open"><?php the_sub_field('accordion_title'); ?></button>
            <p x-show="open"><?php the_sub_field('accordion_content'); ?></p>
        </div>
    <?php endwhile; ?>
<?php endif; ?>

Here’s a real demo:

JavaScript behavior composed directly in your markup!


Tabbed Content

This is similar to an accordion, but we’ll only allow one item to be active at a time:

<?php if( have_rows('tabs') ): ?>
    <div x-data="{active: 0}">
        <!-- Tab buttons -->
        <div class="flex items-center gap-4">
            <?php while( have_rows('tabs') ) : the_row(); $i = 0; ?>
                <button @click="active = <?= $i; ?>"><?php the_sub_field('tab_title'); ?></button>
            <?php $i++; endwhile; ?>
        </div>

        <!-- Tab content -->
        <?php while( have_rows('tabs') ) : the_row(); $i = 0; ?>
            <div x-show="active === <?= $i; ?>">
                <?php the_sub_field('tab_content'); ?>
            </div>
        <?php $i++; endwhile; ?>
    </div>
<?php endif; ?>

And here’s a similar implementation IRL:

This is the first tab's content.
This is the second tab's content.
This is the third tab's content.

Dynamic background color
You can set the background color of each button based on your components state:

<button 
    @click="active = <?= $i; ?>"
    :class="active === <?= $i; ?> ? 'bg-cyan-800' : 'bg-transparent border border-cyan-800'" 
    class="text-cyan-300 px-4 py-2 rounded-md hover:bg-cyan-800"><?php the_sub_field('tab_title'); ?></button>

What’s happening here?
If the value of active is equal to $i, Alpine applies the bg-cyan-800. Otherwise bg-transparent border border-cyan-800 is applied. This is using a ternary operator – a shorthand syntax for if/else. The following examples express the same logic:

someVariable === someValue ? 'Return if true' : 'Return if false';

if (someVariable === someValue) {
    return "Return if true";
} else {
    return "Return if false";
}

Alpine and ACF Inner Blocks

I’ve come across one hiccup when mixing Alpine and ACF Inner Blocks. Here’s the gist of the issue:

Say we’re in the render template for an ACF block with InnerBlocks enabled:

<div x-data="{open: false}">
    <button @click="open = !open">Toggle</button>

    <p x-show="open">Show me</p>
</div>

Ignore the fact that there aren’t any actual InnerBlocks being used. This is just for blocks where jsx is set to true in the block.json. Because our block contains data inside of curly brackets: { }, the block editor will attempt to serialize the data and fail – thus breaking the block.

I see two options here:

  1. If you don’t need Alpine to work for this block in the editor, conditionally remove the x-data directive when viewing the block on the backend:
<?php $alpine_data = (!$is_preview) ? 'x-data="{open: false}"' : ''; ?>

<div <?= $alpine_data; ?>>
    <button @click="open = !open">Toggle</button>

    <p x-show="open">Show me</p>
</div>
  1. If your Alpine logic is complex enough, or if you really just want it to work on the backend, extract the logic into an Alpine.data component. Alpine.data let’s you write your Alpine logic as traditional JavaScript in a traditional JS file.
<div x-data="toggleVisibility">
    <button @click="toggle">...</button>
 
    <div x-show="visible">...</div>
</div>
document.addEventListener('alpine:init', () => {
    Alpine.data('toggleVisibility', () => ({
        visible: false,

        toggle() {
            this.visible = ! this.visible
        }
    }))
})

🤜 🤛 You did it!

If you've followed along, let me know how this went or if you have any questions.
@Joey_Farruggio or joey@joeyfarruggio.com