DEV Community

Cover image for Developing AlpineJs Apps Similar to Vue's Composition API
Konstantinos Zagoris
Konstantinos Zagoris

Posted on • Originally published at wittyprogramming.dev

Developing AlpineJs Apps Similar to Vue's Composition API

Vue's primary motivation behind Composition API's introduction was a cost-free mechanism for reusing logic between multiple components or apps. This idea is based on Functional Composition for programming, and its simplest form is about to produce a function by combining multiple other functions.

This technique is effortless to apply in AlpineJs using a combination of x-data and x-init attributes. First of all, if you never heard of this framework before, check out my introduction AlpineJs article:

Architecture

Let's have as a base example the Netlify Contact Form described in the previous article:

We will reshape the code to separate logic into different modules. The following figure depicts the new architecture:

Alt Text

We separate the code into three modules. One module will validate the form input utilizing another module that contains input validators, and an additional module will take care of the form backend submission.

Validators Module

Let's briefly explain the depicted modules. The most simple module is the validators. It contains functions that validate if input value follows a corresponding rule. It includes four self-explanatory validators (isEmail, isRequired, hasMinLength and hasMaxLength) and the code is:

export function isEmail(value) {
  return new RegExp("^\\S+@\\S+[\\.][0-9a-z]+$").test(
    String(value).toLowerCase()
  );
}

export function isRequired(value) {
    return value !== null && value !== undefined && value.length > 0;
}

export function hasMinLength(value, length) {
  return String(value).length >= length;
}

export function hasMaxLength(value, length) {
  return String(value).length <= length;
}
Enter fullscreen mode Exit fullscreen mode

netlifySubmission Module

The netlifySubmission module handles the backend submission to the netlify service. It exposes a function named submitToNetlify that takes one argument, the form element intended to submit.

The code is taken from the previous article, modified for our module:

export function netlifySubmission() {
  return {
    submitToNetlify(formElement) {
      let body = new URLSearchParams(new FormData(formElement)).toString();
      return fetch("/", {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: body,
      })
        .then((response) => {
          if (response.ok) {
            formElement.reset();
            alert("Thank you for your message!");
          } else {
            throw new Error(`Something went wrong: ${response.statusText}`);
          }
        })
        .catch((error) => console.error(error));
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

formValidator Module

The formValidator module carries out the main work. It has six responsive data attributes that correspond to name, email, and comment input components that compose the submit form. The dirty boolean variables are needed to determine when a user has lost focus from the corresponding input component. It is used to prevent the error messages to appear before the user attempts to enter a value. It is used in the HTML like this:

<p>
        <label
          >Full Name: <input x-model="name" x-on:blur="nameDirty = true" type="text" name="name"
        /></label>
      </p>
      <p x-show.transition="!isNameValid() && nameDirty" style="color: red" x-cloak>
        Please fill out your full name.
      </p>
Enter fullscreen mode Exit fullscreen mode

And it produces the following behavior:

Form Validation Exmple

The full code for the formValidator module is:

import {
  isEmail,
  hasMaxLength,
  hasMinLength,
  isRequired,
} from "./validators.mjs";

export function formValidator() {
  let submitBackend;
  return {
    name: null,
    nameDirty: false,
    email: null,
    emailDirty: false,
    comments: null,
    commentsDirty: false,
    isNameValid(maxLength) {
      return (
        this.nameDirty &&
        isRequired(this.name) &&
        (maxLength ? hasMaxLength(this.name, maxLength) : true)
      );
    },
    isEmailValid() {
      return this.emailDirty && isEmail(this.email);
    },
    isCommentsValid(minLength) {
      return (
        this.commentsDirty &&
        isRequired(this.comments) &&
        (minLength ? hasMinLength(this.comments, minLength) : true)
      );
    },
    isFormValid() {
      return (
        this.isNameValid() && this.isEmailValid() && this.isCommentsValid()
      );
    },
    submitForm() {
      this.nameDirty = true;
      this.emailDirty = true;
      this.commentsDirty = true;
      if (!this.isFormValid()) return;
      submitBackend(this.$el);
    },
    formValidator_init(backend) {
      submitBackend = backend;
      this.$watch("name", (value) =>
        console.log(`The name change to ${value}`)
      );
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Worth noticing is the formValidator_init function, which is executed during the x-init lifecycle, and define the backend submission function.

The above architecture provides a lot of flexibility with the code. For example, to change the backend, only the netlifySubmission module will need to be replaced without any other code modification. Or maybe use the validators to other form inputs in the app.

This design can be implemented in Alpine.js by combing the javascript's object destructuring ability for compositing the object that the x-data attribute expects and the browser's native module support.

Function Composition

Therefore, the final step is to create the AlpineJs component by using the previous modules. The Alpine.js expects a data object in its x-data attribute, which also declares the component scope; accordingly, a function may compose this data object by combining the modules' exported functions and variables.

If it helps, you may think of this function as the setup property of a Vue component, although you cannot use the full capabilities that Vue's setup function provides, for example, an effect or watch feature. For that, the x-init attribute and the $watch magic attribute may be of help. We write inside the HTML file that the form lies the following code:

 <script type="module" defer>
      import { formValidator } from "./form-validator.mjs"
      import { netlifySubmission } from "./netlify.mjs"

      window.contactForm = function() {
         return {
           ...netlifySubmission(),
           ...formValidator(),
          init() {
             this.formValidator_init(this.submitToNetlify);
             this.$watch("email", (value) =>
               console.log(`the value of the email is ${this.email}`)
             );
           }
         }
       }
    </script>
Enter fullscreen mode Exit fullscreen mode

First, we defined a script tag as a module and imported our other modules. In our example, we used the .mjs extension, although the standard .js extension will work as well. I recommend using the .mjs extension as it makes it easier to distinguish modules from regular javascript files. If you want to know more about the module support, check the Mozilla MDN guide.

We must then explicitly assign our function in the window global scope to be accessible from the x-data attribute because we use the module approach. Finally, we compose the final data object from the module's exports using the javascript's spread operator and initialize our formValidator module in the init() function.

The init() function is used in x-init attribute and is executed after the initialization of AlpineJs component. It mimics the mounted() or ngOnInit() hooks from Vue or Angular, respectively. In this function, we may initialize our modules as we do with the formValidator or include some side effects using $watch magic attribute.

Overall, I find this approach very scalable if you want to grow your AlpineJs to something more complicated or reduce some code repetition to your web app.

You may find a complete working version in the following codesandbox:

Top comments (0)