DEV Community

loading...
Cover image for Cross-component Communication Patterns in AlpineJs

Cross-component Communication Patterns in AlpineJs

kzagoris profile image Konstantinos Zagoris Originally published at wittyprogramming.dev ・7 min read

One of the most frequent requirements when writing AlpineJs components is the communication between them. There are various strategies for how to tackle this problem. This article describes the four most common patterns that help pass information between different Alpinejs components.

As a simple example of the presenting patterns, we will create a snackbar component from the material design guidelines. Snackbars are concise, informative messages about some event or the output of a process.

Let's create the snackbar HTML structure and a button that initiates the message.

<body>
    <h1>Material Snackbar Example using Alpine.Js</h1>
    <button class="button">Show Snackbar</button>
    <h2>Click on the above button to see snackbar message</h2>
    <div class="alpine-snackbar-wrapper">
        <div class="alpine-snackbar-content">Sample Text</div>
    </div>
</body>
Enter fullscreen mode Exit fullscreen mode

Next, we add the styles to mimic the material designs.

 body {
        font-family: 'Roboto', sans-serif;
}

.alpine-snackbar-wrapper {
    min-width: 344px;
    max-width: 672px;
    min-height: 48px;
    background-color: #2196F3;
    color: #fff;
    text-align: center;
    margin: auto 8px;
    display: flex;
    align-items: center;
    padding: 0;
    border-radius: 4px;
    position: fixed;
    right: 1%;
    z-index: 1;
    bottom: 30px;
    box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12);
}

.alpine-snackbar-content {
    flex-grow: 1;
    font-size: 0.875em;
    font-weight: 400;
    padding: 14px 16px;
}

.button {
    border: none;
    padding: 14px 16px;
    border-radius: 4px;
    font-size: 1em;
    background-color: #2196F3;
    color: white;
    cursor: pointer;
}
Enter fullscreen mode Exit fullscreen mode

The end result is:

Alt Text

The main goal is to click the button and display the snackbar for a specific time with a custom message. Let's start describing the strategies to achieve it using the AlpineJs framework.

Custom Javascript Event

One apparent approach is to use the javascript standard of dispatching and consuming javascript events on the window object.

This approach's main advantage is the framework-agnostic aspect that may communicate with your AlpineJs components from everywhere in your app. Following the custom events documentation, when the button is clicked, a CustomEvent is created and then dispatched carrying the custom message.

You may check out the source and the result:

The HTML button code is:

<button onclick="showAlpineSnackbar()" class="button">Show Snackbar</button>
Enter fullscreen mode Exit fullscreen mode

Then, we write the showAlpineSnackbar() javascript function in which the CustomEvent is created and dispatched:

function showAlpineSnackbar() {
    let event = new CustomEvent("alpine-snackbar-showMessage", {
        detail: {
            message: "Hello from outside of the component"
        }
    });
    window.dispatchEvent(event);
}
Enter fullscreen mode Exit fullscreen mode

We created a CustomEvent object that defined the event name that we want to dispatch (alpine-snackbar-showMessage) and the information that the event carries (custom message).

The next step involves the creation of the AlpineJs snackbar component.

function alpineSnackbar() {
    return {
        show: false,
        message: null,
        init() {
            window.addEventListener("alpine-snackbar-showMessage", (event) => {
                this.showMessage(event.detail.message);
            });
        },
        showMessage(msg) {
            this.message = msg;
            this.show = true;
            window.setTimeout(() => {
                this.show = false;
                this.message = null;
            }, 2000);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We defined a component scope with two reactive variables:

  • show that sets the snackbar visibility and
  • message that defines the displaying custom message

and two functions:

  • init() and
  • showMessage(msg)

In the init() function executed during the component initialization stage, we create the listener for the custom event. When the event fires, we call the showMessage function with the custom message as an argument, taken from the detail object.

The showMessage function initially sets the custom message and displays the snackbar by setting the show variable as true. Then, we place a timeout function that runs after two seconds and resets the snackbar's state and hides it.

We chose to extract data and behavior into a function, which gives us breathing room for coding. The final step involves wiring up our component to the HTML:

<div x-data="alpineSnackbar()" x-init="init()" x-show.transition="show" class="alpine-snackbar-wrapper">
        <div class="alpine-snackbar-content" x-text="message"></div>
    </div>
Enter fullscreen mode Exit fullscreen mode

The dispatch magic attribute

Another similar approach to custom events is by using the AlpineJs native $dispatch magic attribute. A magic attribute in AlpineJs is a user-defined attribute that implements a helpful operation. There many magic helpers native in AlpineJs or by importing additional packages. One of the native magic attributes is the $dispatch, a shortcut for creating a javascript custom event internally and fire up with a dispatchEvent.

The full snackbar example using the $dispatch magic helper is:

Therefore, by using the $dispatch magic attribute, we drop the function that creates the CustomEvent:

<button x-data @click="$dispatch('alpine-snackbar-show-message', {
        message: 'Hello from outside of the component'
    })" class="button">Show Snackbar</button>
Enter fullscreen mode Exit fullscreen mode

Please note that we utilized the x-data to create an Alpinejs component to use the magic attribute. The $dispatch syntax is straightforward. The first argument corresponds to the CustomEvent name, and the second is the payload that attaches to the event.

The snackbar AlpineJs component that consumes the $dispatch event is transformed:

 <div x-data="alpineSnackbar()" @alpine-snackbar-show-message.window="showMessage($event.detail.message)"
    x-show.transition="show" class="alpine-snackbar-wrapper">
    <div class="alpine-snackbar-content" x-text="message"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

The previously written init() function, executed during the initialization of the AlpineJs component on x-init attribute, is replaced with the equivalent x-on attribute (or its shorthand syntax @). This syntax attaches an event listener to the element it is declared. Therefore, we used the CustomEvent name to catch the event and execute the showMessage(msg) function with the corresponding custom message accessed by the $event object.

You should consider the way that event propagates when you are using the $dispatch. If you need to capture events from elements not under the same node, you need to use the .window modifier. This modifier installs the listener on the global window object instead of the DOM node on which it is declared.

The component magic helper

Another approach to cross-component communication is using the $component from AlpineJs Magic Helpers, a collection of magic properties and helper functions. The installation is simple enough, add the following script tag before the AlpineJs tag.

<script src="https://cdn.jsdelivr.net/gh/alpine-collective/alpine-magic-helpers@0.5.x/dist/component.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

The $component gives access to other AlpineJs components. To achieve this, you provide a unique id for the component you want to access using the x-id attribute.

<div x-id="alpine-snack-bar"
     x-data="{ show : false, message: null }"
     x-show="show"
     class="alpine-snackbar-wrapper">
<div class="alpine-snackbar-content"
     x-text="message"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

Initially, we gave our snackbar component an id (alpine-snack-bar) and defined the two variables that we need. The show and message in x-data control the snackbar component's visibility and content, respectively.

Then, in the button we write a function inside the buttonShowSnackbar component that display the snackbar with the appropriate message and hides after two seconds. The HTML code is:

<button x-data="buttonShowSnackbar()"
        @click="showAlpineSnackbar('alpine-snack-bar')"
        class="button">Show
        Snackbar</button>
Enter fullscreen mode Exit fullscreen mode

The buttonShowSnackbar component:

function buttonShowSnackbar() {
    return {
        showAlpineSnackbar(id) {
            this.$component(id).message = "Hello from another component using the $component";
            this.$component(id).show = true;
            setTimeout(() => {
                this.$component(id).show = false;
                this.$component(id).message = null;
            }, 2000)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We need to define the showAlpineSnackbar function inside an AlpineJs component to access the $component magic helper.

You can check the full example:

Having a global state by using the spruce library

Finally, another cross-communication pattern between AlpineJs components is by using a global state. For AlpineJs, there is spruce, an excellent global state management library from Ryan Chandler.

The installation is more or less the same. Add the spruce library script tag before the AlpineJs tag.

<script src="https://cdn.jsdelivr.net/npm/@ryangjchandler/spruce@2.x.x/dist/spruce.umd.js"
        defer></script>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js"
        defer></script>
Enter fullscreen mode Exit fullscreen mode

The approach is very similar to the $component pattern. The only difference is that we manipulate the global state instead of a specific component state. Initially, we initialize our global state:

Spruce.store("snackbar", {
    message: null,
    show: false
});
Enter fullscreen mode Exit fullscreen mode

We create a store called snackbar with two state variables, the message to be shown and the show that controls the snackbar visibility.

The HTML part of the buttonShowAlpineSnackbar() component is exactly the same as before:

<button x-data="buttonShowAlpineSnackbar()" @click="showMessage('A message from spruce')" class="button">Show Snackbar</button>
Enter fullscreen mode Exit fullscreen mode

The difference is that the showMessage(msg) function manipulates the snackbar store state:

function buttonShowAlpineSnackbar() {
    return {
        showMessage(msg) {
            this.$store.snackbar.message = msg;
            this.$store.snackbar.show = true;
            window.setTimeout(() => {
                this.$store.snackbar.show = false;
                this.$store.snackbar.message = null;
            }, 2000);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The snackbar component is similar to the previous example, except that the snackbar store state controls the global state's visibility and message content.

<div x-data x-show.transition="$store.snackbar.show" class="alpine-snackbar-wrapper">
  <div class="alpine-snackbar-content" x-text="$store.snackbar.message"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

You can check the full example:

Conclusions

I presented four different approaches for cross-component communication. Although you can achieve the same outcome with every one of the above patterns, I think each one is better under a specific requirement. For example, the custom event javascript pattern is best suited for passing information between AlpineJs components and vanilla javascript code (or maybe another framework). Otherwise, the $dispatch magic attribute is more appropriate as it reduces the CustomEvent's boilerplate code. When you want to manipulate a specific component, the $component magic helper is most suitable as it gives you direct access to it. Finally, the spruce library is better suitable for apps that need global state.

Discussion (0)

pic
Editor guide