/ #alpinejs #javascript 

Alpine.js + jQuery/JavaScript Plugin Integration: a Select2 example

One of the jQuery ecosystem’s greatest strength is the wealth of drop-in plugins available.

Alpine.js is a great way to phase out jQuery spaghetti code from current and future projects with its declarative nature and small bundle size.

What Alpine doesn’t have (yet), is a thriving plugin ecosystem. However, it’s all “just JavaScript” and it’s completely possible to leverage jQuery plugins in Alpine.js components.

There have been a slew of questions in the Alpine.js Issues about integrating 3rd party libraries and jQuery plugins. This post will go through the example of integrating Select2 - The jQuery replacement for select boxes although the principles and tools explained should work for other plugins and tools.

This post is “choose your own adventure”, if you’re interested in a CodePen or copy-pasting the code, head straight to Show me the code. If on the other hand you’re interested in the step by step breakdown of the integration, that follows the first code/demo section.

Table of Contents

Show me the code

Here’s a Select2 input whose selection state is synced to Alpine.js and which reflects Alpine.js state.

See the Pen Alpine.js + jQuery select2 by Hugo (@hugodf) on CodePen.

Here’s the full working code in the “No Script Tag Here” style, ready to copy paste. If you’re interested in the step by step of how we came to this, read the next sections.

<div
  x-data="{ selectedCity: '' }"
  x-init="() => {
    // use a function so we can return a 'destroy' callback
    select2 = $($refs.select).select2();
    select2.on('select2:select', (event) => {
      selectedCity = event.target.value;
    });
    $watch('selectedCity', (value) => {
      select2.val(value).trigger('change');
    });
    return () => {
      select2.select2('destroy');
    };
  }"
>
  <select x-ref="select" data-placeholder="Select a City">
    <option></option>
    <option value="London">London</option>
    <option value="New York">New York</option>
  </select>
  <p>Selected value (bound in Alpine.js): <code x-text="selectedCity"></code></p>
  <p>
    <button @click="selectedCity = ''">
      Reset selectedCity
    </button>
  </p>
  <p>
    <button @click="selectedCity = 'London'">
      Trigger selection of London
    </button>
  </p>
  <p>
    <button @click="selectedCity = 'New York'">
      Trigger selection of New York
    </button>
  </p>
</div>

Step by step integration of jQuery plugin Select2 with Alpine.js

Our select2 integration works as follows, this is a step by step tutorial, the full working code is in the previous section and in the Alpine.js + jQuery select2 CodePen.

0. Setting the scene

We’ll start with an Alpine.js/jQuery/Select2 app which allows us to pick a city by clicking buttons.

Note that we would usually be able to 2-way bind the select element using x-model, but this is not the purpose of this exercise.

The goal is for Alpine.js state (selectedCity) and a Select2 box initialised on the “Select a City” select to always reflect each other’s state (2-way binding). That is:

  • if we select a value using the Select2 box, the value should update in the Alpine.js application state
  • if we update the value of Alpine.js state (selectedCity), this update should be reflected in the Select2 selection.

Here’s the initial code, which doesn’t work.

<div x-data="{ selectedCity: '' }">
  <select data-placeholder="Select a City">
    <option></option>
    <option value="London">London</option>
    <option value="New York">New York</option>
  </select>
  <p>Selected value (bound in Alpine.js): <code x-text="selectedCity"></code></p>
  <p>
    <button @click="selectedCity = ''">
      Reset selectedCity
    </button>
  </p>
  <p>
    <button @click="selectedCity = 'London'">
      Trigger selection of London
    </button>
  </p>
  <p>
    <button @click="selectedCity = 'New York'">
      Trigger selection of New York
    </button>
  </p>
</div>

1. Add an x-ref on the element to initialise with jQuery Select2

Our first step is to add a ref on the select that we’ll turn into a select2 box using x-ref="select", we can now access this element using $refs.select or this.$refs.select.

<div x-data="{ selectedCity: '' }">
  <select x-ref="select" data-placeholder="Select a City">
    <!-- options etc, no change -->
  </select>
  <!-- rest of template, no change -->
</div>

2. Add an x-init handler + initialise the Select2 box

Next, we add an x-init handler to our Alpine.js component.

In x-init we initialise the Select2 box by creating a jQuery instance (using $(element)) from $refs.select (which we defined in the previous step) and calling .select2() on it. In full: $($refs.select).select2().

We set the output of .select2() as the select2 instance variable in order to access it later.

<div
  x-data="{ selectedCity: '' }"
  x-init="
    select2 = $($refs.select).select2();
  "
>
  <!-- rest of template -->
</div>

The issue we’re now facing is that the Select2 box can update without the Alpine.js state being updated. We’ll fix this in the next step.

3. Listen to select2:select events and update selectedCity Alpine.js state

In order to update Alpine.js state when a Select2 selection happens (select2:select event), we’ll use our select2 instance and use the jQuery .on() method to add a listener for selection events.

When select2:select event occurs, we’ll run a callback that updates selectedCity to the event.target.value.

<div
  x-data="{ selectedCity: '' }"
  x-init="
    // select2 instantiation
    select2.on('select2:select', (event) => {
      selectedCity = event.target.value;
    });
  "
>
  <!-- no template changes -->
</div>

Changes to the Select2 box are now reflected in Alpine.js state as per the following screen capture.

Selecting a city in the Select2 box reflects in Alpine.js state

We’ve now seen how to sync selection data from Select2 to Alpine.js using .on('select2:select', callback).

Note that when we click the “Reset” and “Trigger” buttons, Select2 doesn’t update, that’s because Alpine.js state updates don’t get synced back to Select2.

In the next section we’ll see how to synchronise Alpine.js state changes to Select2 using $watch and jQuery’s .val().trigger() methods.

4. $watch and set value with .val().trigger()

The previous sections we created a select2 instance using x-init and registered a listener on select2:select which sets Alpine.js selectedCity state to the selection data.

The final step in this guide is to make sure that changes to Alpine.js state are reflected/synchronised to Select2’s state.

In order to do this, we’ll add a $watch('selectedCity', callback) expression in x-init (after instantiating the select2).

In the $watch callback, we’ll set the value of the select2 instance with the .val() method and trigger an update using .trigger('change'). Put together the callback is (value) => select2.val(value).trigger('change'), ie. when selectedCity changes, take the new value, set it as select2’s value and trigger an update.

<div
  x-data="{ selectedCity: '' }"
  x-init="
    // select2 instantiation & listener binding
    $watch('selectedCity', (value) => {
      select2.val(value).trigger('change');
    });
  "
>
  <!-- no template changes -->
</div>

Changes to Alpine.js selectedCity state are now reflected in the Select2 box as per the following screen capture.

Changing the selectedCity in Alpine.js state updates the Select2 box selection

We’ve now looped the loop:

  • Select2 gets initialised when the Alpine.js component initialises
  • Select2 updates are synchronised to Alpine.js state
  • Alpine.js state updates are reflected in the Select2 box

Next we’ll look at an optional plugin teardown step that is crucial for production use-cases.

5. (Optional) Destroying the Select2 plugin instance on Alpine.js unmount

Sample use cases of Select2 don’t require the following step since the only thing we do in the demo is mount the Alpine.js component and it’s the only thing displaying on the page.

However for production use-cases, it might be useful to get rid of the select2 instance we create during x-init.

In order to do this, we need to change our x-init to be a function instead of a set of statements.

We can then return the “on unmount”/“on destroy” function from this function:

() => {
  // things to do on init
  return () => {
    // things to do on destroy
  }
}

In our “unmount”/“destroy” function, we can destroy the select2 instance by calling .select2('destroy').

<div
  x-data="{ selectedCity: '' }"
  x-init="() => {
    // x-init is now a function, no other changes
    select2 = $($refs.select).select2();
    select2.on('select2:select', (event) => {
      selectedCity = event.target.value;
    });
    $watch('selectedCity', (value) => {
      select2.val(value).trigger('change');
    });
    return () => {
      select2.select2('destroy');
    };
  }"
>
  <!-- no template changes -->
</div>

We’ve now seen how to tear down any plugins used through Alpine.js when the Alpine.js component unmounts.

Next we’ll recap the steps that we’ve just gone through.

Wrapping up - Summary of an Alpine.js + JavaScript/jQuery plugin integration

The steps to integrating a jQuery/JavaScript plugin like Select2 are as follows:

  1. Add x-ref to the element on which you’re going to initialise the plugin, this makes it easier to access through $refs/this.$refs.
  2. Initialise the plugin in x-init making sure to keep the instance around, with jQuery plugins that usually means instance = $($refs.pluginElement).pluginName(/* plugin options */), note the $() around the ref, which wraps the DOM Node/element in a jQuery instance.
  3. If you need to synchronise plugin updates -> Alpine.js state, add listeners for the relevant events on the plugin instance & update Alpine.js state in the listener callback. For jQuery plugins where events are namespaced with the plugin name: instance.on('plugin:event', (event) => { alpineState = event.target.value }).
  4. If you need to synchronise Alpine.js state -> plugin, add the relevant $watch('alpineState', callback) expression in this watcher callback, trigger updates on the plugin instance. In most jQuery plugins that means instance.val(value).trigger('change').
  5. If you’re keen to clean up whatever your Alpine.js component initialised, use an x-init function so that you an return a destroy callback which will call the plugin’s “destroy”. With jQuery’s norm of instance.trigger('destroy'), the callback you need to return will be: () => { instance.trigger('destroy') }.

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-logoClay Banks

Author

Hugo Di Francesco

Co-author of "Professional JavaScript" with Packt. He 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 and Elsevier.

Interested in Alpine.js?

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