(Updated: )
/ #alpinejs #javascript 

A guide to Alpine.js component communication

Learn how to share information between Alpine.js components with the $dispatch magic property and the window/document as an event bus.

This post will show how to trigger and listen to global/window/document events with Alpine.js in order to use it as an event bus to communicate between sibling components (which is the only type of component composition that Alpine.js supports)

Alpine.js is a great lightweight option for declarative view rendering. One of the first non-documented use-cases Alpine adopters come across is how to communicate between different Alpine.js components on the page.

For example a “flash” message (or “toast” in Material UI parlance) component but want to be able to trigger it from anywhere on the page (not just from inside its body). Other common occurences are triggering modals to open/close from outside of them or submitting a form from another part of the page (that’s a separate Alpine.js component).

Alpine.js, unlike React or Vue, doesn’t provide any parent-child communication patterns or primitives. “Alpine components” don’t receive any context from parent components, each component has its own individual and independent scope which means nesting doesn’t work as you would expect coming from Vue/React or even JavaScript (JavaScript has lexical scoping, Alpine does not).

That means that in Alpine.js there’s no parent-child component communication only “sibling” component communication. For sibling communication, the pattern often proposed in issues by Caleb (the creator of Alpine.js) and other contributors is to use the window or document as an event bus.

Table of Contents

“window as event bus” Alpine.js component communication pattern

As the “window as event bus” name states, the pattern consists of using the window (or document) as an event bus.

An event bus is a name for an entity on which listeners can be registered (& unregistered) and events can be emitted. When an event is triggered, the relevant listeners are run.

In the case of Alpine.js window or document is an event bus due to the following:

  1. listeners can be registered for events on the window/document using x-on:event-name.window or x-on:event-name.document respectively (here the event’s name is event-name).
  2. events can be emitted to the window or document through the use of the $dispatch magic property and the browser’s event bubbling behaviour (most events bubble up to the document/window unless their propagation is stopped).

For more information about Event Bubbling see Event bubbling and capture on MDN.

Here’s how the “window as event bus” patterns works:

  • When we want to send a (custom) event to another component, we use $dispatch('event-name', { data: 'properties' }).
  • In the component that receives the events, we register a listener using the .window or .document modifier.

For example the following code has a hardcoded alert that can be toggled from anywhere on the page (as a demo we’ve got 2 other Alpine.js components).

The way it achieves this is to have x-on:toggle.window="isOpen = !isOpen", which updates the “alert” component’s state when a toggle event bubbles up to the window.

In the other Alpine.js components, all it takes is to $dispatch('toggle') when we want to toggle the “alert”’s visibility.

<div x-data="{ isOpen: false }">
  <!-- this component can be shown/hidden using a `toggle` event  -->
  <div
    x-show="isOpen"
    x-on:toggle.window="isOpen = !isOpen"
    role="alert"
    >
    <p>This alert is toggled when `toggle` events are dispatched.</p>
    <button @click="isOpen = false">close alert</button>
  </div>
</div>
<div x-data="{}">
  <p>The button in this component can toggle the "alert".</p>
  <button @click="$dispatch('toggle')">Toggle alert</button>
</div>
<div x-data="{}">
  <p>The button in this component can <strong>also</strong> toggle the "alert".</p>
  <button @click="$dispatch('toggle')">Toggle alert 2</button>
</div>

The following CodePen shows this code in action.

See the Pen Alpine.js "window as event bus" demo by Hugo (@hugodf) on CodePen.

We’ve now seen what an event bus is and how the browser window/document can be considered one in the context of Alpine.js. We’ve also seen a limited example of how this pattern can be used.

Next we’ll see how to pass data to use to update the state in the listener component with the example of a flash message/toast which can be passed message and level data.

Pass data from one Alpine.js component to another: a flash message/toast component

The $dispatch magic property supports 2 parameters when called:

  1. the event (name) (as a string) - the name of the event, also the event’s type.
  2. (optional, defaults to {}) detail object for the event - the data to be set as event.detail.

To pass data using the “window as event bus” Alpine.js pattern, we can use the detail parameter when dispatching the event and then read from it in the listening component using $event.detail.

In other words:

  • to emit: $dispatch('event-name', { hello: 'world' })
  • to read: x-on:event-name="console.log($event.detail.hello)"

To illustrate this, here’s a full flash message/toast example that listens to flash events and updates the message/level using $event.detail.level and $event.detail.msg. The example also has multiple triggers sending different types of events.

<div x-data="{ msg: '', level: '' }">
  Flash Component
  <template x-on:flash.window="msg = $event.detail.msg; level = $event.detail.level;"></template>
  <template x-if="msg && level">
    <div role="alert" class="mt-2">
      <div class="text-white font-bold rounded-t px-4 py-2 capitalize" :class="{'bg-red-500': level === 'error', 'bg-blue-500': level === 'info'}" x-text="level">
      </div>
      <div class="border border-t-0 rounded-b px-4 py-3" :class="{'bg-red-100 text-red-700 border-red-400': level === 'error', 'bg-blue-100 text-blue-700 border-blue-400': level === 'info'}">
        <p x-text="msg"></p>
      </div>
    </div>
  </template>
</div>
<div x-data="{}">
  Trigger 1
  <button @click="$dispatch('flash', { level: 'info', msg: 'This is an info message' })" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
    Flash Info
  </button>
</div>
<div x-data="{}">
  Trigger 2
  <button @click="$dispatch('flash', { level: 'error', msg: 'This is an error message' })" class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">
    Flash Error
  </button>
</div>
<div x-data="{}">
  Trigger 3
  <button @click="$dispatch('flash', { level: '', msg: '' })" class="font-bold py-2 px-4 rounded">
    Clear Flash
  </button>
</div>

The following CodePen shows this code in action.

See the Pen Alpine.js "window as event bus" flash message/toast by Hugo (@hugodf) on CodePen.

We’ve now seen how to pass data using the “window as event bus” Alpine.js pattern.

Next we’ll look at avoiding some pitfalls of HTML/Alpine.js/events interoperability with naming conventions.

Conventions for naming events that are triggered on the “window as event bus”

It’s quite common for developers who discover the “window as event bus” pattern to want to use a name that’s camelCased like eventName.

Unfortunately, that won’t work, since Alpine.js event listeners are registered using HTML attributes and HTML attributes are case insensitive. If we register x-on:eventName, as far as HTML is concerned it’s x-on:eventname. When we $dispatch('eventName'), we’ll have a mismatch, since we’re listening for eventname but triggering eventName.

I therefore recommend using a dash-cased name like event-name.

Another convention that comes in useful is to namespace your event using :. For example triggering an update event in Spruce - A lightweight state management layer for Alpine.js actually creates and dispatches a spruce:update event. In this case spruce is the namespace and update is the event type.

Namespacing is useful to avoid event name collisions. For example if both Spruce and Alpine trigger update events that contain different data and mean different things, how do we figure out which is which?

Using the namespace:event-name convention is especially important if the events you’re emitting are very generic (eg. update, created etc.), collide with a browser event (eg. click, input, submit, select etc.), you want to make it obvious who the intended target is (the listening component) or your listening component listens to multiple types of events. In these cases using a namespaced event will be incredibly useful.

We’ve now seen some event naming conventions that can help keep your “window as event bus” implementation clean and understandable for longer.

Next we’ll see what you can do if the event bus pattern is becoming unwieldy.

Going beyond the event bus: global stores with Spruce

Spruce - A lightweight state management layer for Alpine.js can be used to create global stores that all your components can access.

That means that the need for a global event bus isn’t as necessary since the state can be shared across components. What’s more Spruce provides its own event bus implementation see Spruce - Event Bus.

For example if you follow the Spruce CDN install instructions, you can then create a store that keeps track of which section should be opened and use that shared state to only show the modal that is selected:

<div x-data="{}" x-subscribe>
    <div x-show="$store.modal.open === 'login'">
    <p>
      This "login" modal isn't built with a11y in mind, don't actually use it
    </p>
    </div>
</div>
<div x-data="{}" x-subscribe>
    <div x-show="$store.modal.open === 'register'">
    <p>
      This "register" modal isn't built with a11y in mind, don't actually use it
    </p>
    </div>
</div>
<div x-data="{}" x-subscribe>
  <select x-model="$store.modal.open">
    <option value="login" selected>login</option>
    <option value="register">register</option>
  </select>
</div>
<script>
  Spruce.store('modal', {
    open: 'login',
  });
</script>

The following CodePen shows this code in action.

See the Pen Spruce demo by Hugo (@hugodf) on CodePen.

We’ve now seen how to use Spruce to share state between components instead and syncing the state using the “window as event bus” Alpine.js pattern.

That’s it for this post, you can check out the Alpine.js tag on Code with Hugo for more in-depth Alpine.js guides.

If you’re interested in Alpine.js, Subscribe to Alpine.js Weekly. A free, once–weekly email roundup of Alpine.js news and articles.

unsplash-logoPavan Trikutam

Author

Hugo Di Francesco

Co-author of "Professional JavaScript", "Front-End Development Projects with Vue.js" with Packt, "The Jest Handbook" (self-published). Hugo runs the Code with Hugo website helping over 100,000 developers every month and holds an MEng in Mathematical Computation from University College London (UCL). He has used JavaScript extensively to create scalable and performant platforms at companies such as Canon, Elsevier and (currently) Eurostar.

Interested in Alpine.js?

Subscribe to Alpine.js Weekly. A free, once–weekly email roundup of Alpine.js news and articles