DEV Community

loading...
Cover image for Kentico Xperience Xplorations: Why We Should Be Using AlpineJs

Kentico Xperience Xplorations: Why We Should Be Using AlpineJs

seangwright profile image Sean G. Wright ใƒป11 min read

In this post we'll be exploring why AlpineJs is an ideal JavaScript library for integrating server-side HTML rendering and client-side JavaScript interactivity.

๐Ÿงญ Starting Our Journey: Choosing Our Tools

For many ASP.NET developers, JavaScript on the web is synonymous with jQuery, and while jQuery certainly is ubiquitous, it's not the only option out there.

The primary benefits of jQuery in 2021 are its ease of use (just add a <script> element to the page!) and its vast plugin ecosystem. However, there are also some drawbacks that come with this library ๐Ÿ˜ฎ.

๐Ÿ jQuery Spaghetti

jQuery is largely concerned with providing a consistent (and flexible) API for manipulating the DOM and using browser features through JavaScript. It takes an imperative and procedural approach to these concerns, because jQuery is a low-level library.

The benefits of a consistent DOM API across browsers doesn't really apply to us anymore in the era of modern, evergreen browsers ๐Ÿ™๐Ÿป.

Likewise, browsers have adopted the jQuery DOM selector syntax (Sizzle) in the document.querySelector() and document.querySelectorAll() functions ๐Ÿ˜Ž.

With these no-longer-benefits out of the way, what do we typically end up with when using jQuery in 2021?

Unfortunately, sometimes it's not pretty ๐Ÿ™„.

The pattern of building something with jQuery typically involves these steps:

  1. Find some DOM elements (often by id or class)
  2. Register some event handlers with those DOM elements so we can react to user interactions on the page
  3. Write logic in JavaScript that is specific to our application
  4. Repeat

Steps 1 and 2 are the ones that become more problematic as the complexity of our JavaScript grows.

Since jQuery is a low-level library, we are responsible for all the plumbing ๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ”ง work. Plumbing is all the code we have to write to 'hook things up', and this plumbing gets spread throughout our business logic.

Not only does this make the important part of our code (the business logic) more confusing, but it's also something we need to maintain over the life of the application.

Tangled spaghetti noodles

A tasty meal, but not the ideal application architecture.
Photo by mindaugas

The term 'jQuery spaghetti' is meant to describe the kind of code we end up being forced to write when trying to build complex UIs with jQuery because the business logic code and plumbing code are all mixed together, and often tightly coupled.

Here's an example of jQuery spaghetti (maybe not a full plate ๐Ÿ):

<form id="myForm">
  <input id="email" type="email" name="email" />
  <span class="error" style="display: none"></span>

  <button type="submit">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode
$(function () {
    // Find our form
    const formEl = $('#myForm');

    if (!formEl) {
      console.error('Could not find form');
      return;
    }

    // Register an event listener
    $('#myForm').on('click', function (e) {
      e.preventDefault();

      // Find our form field
      const emailEl = $('form [name="email"]');

      if (!emailEl) {
        console.error('Could not email form field');
        return;
      }

      // Get the email value
      const email = emailEl.val();

      // find the error element
      const errorEl = $('form .error');

      if (!errorEl) {
        console.error('Could not find error message element');
        return;
      }

      if (!email) {
        // set the error message
        errorEl.text('An email address is required');
        errorEl.show();
      } else {
        errorEl.hide();
      }
    });
});
Enter fullscreen mode Exit fullscreen mode

The code above is almost entirely focused on plumbing ๐Ÿ’ฆ, with only a few lines (checking for an email address and showing the error message) of business logic.

If we change the location of our error element and move it out of the form, our code stops working. If we change the class (error) on our error element, our code stops working.

Yes, there are best practices to help avoid these problems, but the fact remains that building something in jQuery requires engineering vigilance, careful decision making, a bit of work to just 'hook' things up ๐Ÿ˜•.

It does not lead to developers walking near the pit of success.

So what are our alternatives ๐Ÿค”?

๐Ÿ’ป A Modern Component Framework

Modern JavaScript frameworks like Svelte React, Vue, and Angular were designed to help us solve the jQuery spaghetti problem.

These frameworks take care of all the plumbing and provide developers with APIs and patterns to ensure their business logic is not littered with finding DOM elements, hooking up event listeners, and explicitly updating the UI.

By taking on the responsibility of plumbing, these frameworks allow developers to grow their JavaScript code in both size and complexity in maintainable ways that result in readable code ๐Ÿ˜€.

The same functionality that we wrote in jQuery would look like this in Vuejs (including the HTML template for rendering):

<template>
  <form @submit.prevent="onSubmit">
    <input id="email" v-model="email" type="email" name="email" />
    <span v-show="error">{{ error }}</span>

    <button type="submit">Submit</button>
  </form>
</template>

<script>
export default {
  data() {
    return { email: '', error: '', };
  },

  methods: {
    onSubmit(e) {
      this.error = !this.email
        ? 'An email address is required'
        : '';
      }
    },
  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

Notice how there's no plumbing here ๐Ÿง! The connection between the HTML and our JavaScript is declarative. We indicate we want the onSubmit function to handle submission of the form by adding the @submit="onSubmit" attribute (directive) to the <form> element.

We also get access to the email input value and the error message by using the v-model="email" binding syntax and {{ error }} interpolation syntax, respectively.

This leaves us with some HTML enhanced by special syntax, which fortunately doesn't rely on HTML id or class attributes, and some business logic (the contents of our onSubmit function).

We are free to re-structure our HTML, change styles, and modify our business logic - all independently ๐Ÿ‘๐Ÿฝ.

I'm a huge fan of browser based client-side HTML rendering frameworks like these, but they unfortunately can pose another problem ๐Ÿค”!

Before it's starts to sound like I'm advising against using any of the above frameworks, I want to clarify that I regularly use Vue in my Kentico Xperience applications and these sites have benefited significantly from our team's adoption of it ๐Ÿ’ช๐Ÿฟ.

If you want to learn more, read my post Kentico 12: Design Patterns Part 16 - Integrating Vue.js with MVC

These frameworks enable the functionality of jQuery without having to write the plumbing code, but unfortunately at the cost of losing control over the rendering of the DOM.

While jQuery can be used to create new DOM elements, it is most often used to change the state of elements already in the page.

Modern JavaScript frameworks like Vue, on the other hand, need to render all their DOM from scratch when they are loaded on the page ๐Ÿคจ.

If we were to look at the HTML send from the server for a traditional Single Page Application (SPA), we would see something like this:

<!DOCTYPE html>
<html>
<head>
    <!-- references to our JavaScript app and libraries -->
</head>
<body>
    <div id="app"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

All the UI of the page is created by the framework as children of the <div id="app"></div> element, and this is what is meant by the phrase 'client-side rendering'.

This means that search engine crawlers would need to execute the JavaScript to see the final HTML and content of the page, and even if the search engine is able to run our JavaScript, it might penalize us for taking too long to render everything ๐Ÿคฆโ€โ™€๏ธ.

This is in stark contrast to server-rendered HTML where the data sent from the server to the browser is going to include everything displayed to the user, so there are no requirements to execute JavaScript or delays to see what it renders on the page.

We ideally would like a library that exists somewhere in between the plumbing free coding of modern JavaScript frameworks, and jQuery's ability to manipulate and create DOM without owning it... ๐Ÿ˜‰

๐Ÿ† AlpineJs Enters the Chat

AlpineJs fits our requirements exceptionally, and is described as offering us

the reactive and declarative nature of big frameworks like Vue or React at a much lower cost.

and

You get to keep your DOM, and sprinkle in behavior as you see fit.

Well, this sounds great ๐Ÿ‘๐Ÿพ. So, how do we use it?

๐Ÿ—บ Our Destination: Using AlpineJs

Let's look at our HTML form example again, but this time with AlpineJs!

First we need to add a <script> element within the document's <head>:

<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.8.0/dist/alpine.min.js" defer></script>
Enter fullscreen mode Exit fullscreen mode

Then, we define a createFormComponent() function, which is where our component state and methods are initialized:

<script>
  (function () {
    'use strict';

    window.createFormComponent = function () {
      return {
        email: '',
        error: '',

        onSubmit($event) {
          this.error = !this.email 
            ? 'You must enter an email address'
            : '';
        },
      };
    };
  })();
</script>
Enter fullscreen mode Exit fullscreen mode

Finally, we annotate our server-rendered HTML with some Alpine specific syntax:

<form id="myForm"
  x-data="createFormComponent()" 
  @submit.prevent="onSubmit">

  <input id="email" type="text" name="email" 
    x-model="email" />

  <span class="error" style="display: none"
    x-show="error"
    x-text="error"
  ></span>

  <button type="submit">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Let's look at each part in detail!


The AlpineJs script works like most JavaScript libraries that we load into the browser without a build process - as soon as the script executes, it looks for "Alpine" stuff and initializes everything it can find on the page.

<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.8.0/dist/alpine.min.js" defer></script>
Enter fullscreen mode Exit fullscreen mode

This means that we can have a page full of existing HTML, rendered on the server and Alpine can hook into it and enable all of its powerful features ๐Ÿ˜„!

Alpine will look for initialization code (defined by x-data directives on our HTML), which can be an inline expression or a function defined the window object.

Speaking of initialization functions, let's look at ours next:

<script>
  (function () {
    'use strict';

    window.createFormComponent = function () {
      return {
        email: '',
        error: '',

        onSubmit($event) {
          this.error = !this.email 
            ? 'You must enter an email address' 
            : '';
        },
      };
    };
  })();
</script>
Enter fullscreen mode Exit fullscreen mode

This block defines an IIFE ๐Ÿค“ (an immediately invoked function expression), which assigns a function (createFormComponent) to the window object so that it's accessible to Alpine (functions and variables defined in an IIFE are not accessible outside of it).

The function we defined, createFormComponent, returns an object that includes the 'state' (email, and error) of our component. These are the values that Alpine ensures stay updated based on user interactions, and also ensures result in an update of the HTML when they change. This is the kind of plumbing we want to avoid, and thankfully Alpine takes care of it for us ๐Ÿคฉ.

Our initialization function also defines a method, onSubmit, that can be called when the user interacts with the component in a specific way.

Note how it sets the value of this.error, which is the error: '' value in our component state.

It also has access to this.email which is the email: '' value in our component state.

Now we can look at our enhanced HTML form:

<form id="myForm"
  x-data="createFormComponent()" 
  @submit.prevent="onSubmit">

  <input id="email" type="text" name="email" 
    x-model="email" />

  <span class="error" style="display: none"
    x-show="error"
    x-text="error"
  ></span>

  <button type="submit">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Alpine connects data and interactivity to our HTML with directives, so let's go through each directive we are using, step-by-step.

<form id="myForm"
  x-data="createFormComponent()" 
  @submit.prevent="onSubmit">

  <!-- ... -->

</form>
Enter fullscreen mode Exit fullscreen mode

The x-data="createFormComponent()" tells Alpine to initialize this <form> element and all of its children elements into a component, and set the state and methods they can access to whatever was returned by createFormComponent() (in our case this is email, error, and onSubmit).

The @submit.prevent="onSubmit" connects our component's onSubmit() function to the submit event of the form (and also ensures $event.preventDefault() is called automatically with the .prevent event modifier ๐Ÿ˜Ž!)

<input id="email" type="text" name="email" 
  x-model="email" />
Enter fullscreen mode Exit fullscreen mode

We make sure the value of the <input> always stays up to date with our component's email: '' value by using the x-model="email" binding syntax. If our JavaScript changes email, the <input> element will immediately reflect that change - if the user types a new value into to <input> our JavaScript will have access to that new value.

<span
  class="error"
  style="display: none"
  x-show="error"
  x-text="error"
></span>
Enter fullscreen mode Exit fullscreen mode

We do something similar with the <span class="error"> by conditionally showing it with x-show="error" which will show the element when our component's error: '' value is truthy and hide it when it is falsy.

The x-text directive sets the innerText of our <span> to whatever the value of error is.

Notice how none of our HTML is connected to our JavaScript through HTML id or class attribute values, which means it's not brittle to updating design or styles ๐Ÿง .

We also don't imperatively connect interactions with our HTML, or the values of our HTML. Instead, Alpine does all the plumbing ๐Ÿšฟ for us and we get to use our ๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป developer skills to focus on business logic.

Here's a live demo of our AlpineJs solution:

Integration With Xperience

If we wanted to populate the error message for our form from Xperience, we could use Razor to set the value, since everything on the page is rendered on the Server:

const errorMessage = '@Model.FormErrorMessage';

window.createFormComponent = function () {
  return {
    email: '',
    error: '',

    onSubmit($event) {
      this.error = !this.email 
        ? errorMessage 
        : '';
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

We can also make requests to our ASP.NET Core API, use the response to set our error message, and our form will be asynchronously validated:

window.createFormComponent = function () {
  return {
    email: '',
    error: '',

    async onSubmit($event) {
      const result = await fetch(
        '/api/form', 
        { 
          method: 'POST', 
          body: JSON.stringify({ email: this.email }) 
        })
        .then(resp => resp.json());

      this.error = result.errorMessage;
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

Or, imagine a scenario where we have a <table> of data and we want to filter the results based on what a user types into an <input>:

<div x-data="initializeFilterComponent()">
  <label for="filter">Filter:</label>
  <input id="filter" type="text" x-model="filter">

  <table>
    @foreach (var row in Model.Rows)
    {
      <tr x-show="isRowVisible('@row.Title')">
        <td>@row.Title</td>
      </tr>
    }
  </table>
</div>

<script>
(function () {
  'use strict';

   window.initializeFilterComponent = function () {
     return {
       filter: '',

       isRowVisible(title) {
         return title
           .toLowerCase()
           .includes(this.filter.toLowerCase());
       }
     }
   };
}());
</script>
Enter fullscreen mode Exit fullscreen mode

In the example above, all of the table rows are initially displayed (great for SEO!) and are only filtered when the user starts typing in the <input> ๐ŸŽ‰!

โœˆ Heading Home: Which Option Is the Best Choice?

Now that we've seen several different options for enhancing our HTML with JavaScript, which one makes the most sense for our use-case?

jQuery

  • If we only need a few lines of code
  • Existing jQuery plugins handle most of the complexity
  • No state management in our 'components'

Vue/React/Svelte/Angular

  • Complex state management
  • Many components interacting together
  • Client-side rendering is ok (SEO is not important)

AlpineJs

  • More than a few lines of code, business logic
  • SEO is important
  • Custom UX (not from a jQuery plugin)

At WiredViews, we've been using AlpineJs in our Kentico Xperience projects, alongside Vuejs, and jQuery.

I recommend using the right tool ๐Ÿ‘ฉ๐Ÿผโ€๐Ÿ”ง for the right job, and fortunately AlpineJs fits in great with modern web development and Kentico Xperience.

As always, thanks for reading ๐Ÿ™!


Photo by Jordan Madrid on Unsplash

References


We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!

If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.

Or my Kentico Xperience blog series, like:

Discussion (0)

Forem Open with the Forem app