Developing AlpineJs Apps Similar to Vue's Composition API 
Tags: javascript, html, vue, netlify
Published: 2021-03-14 07:27:00

[Vue's primary motivation behind Composition API's](https://composition-api.vuejs.org) introduction was a cost-free mechanism for reusing logic between multiple components or apps. This idea is based on [Functional Composition](https://en.wikipedia.org/wiki/Function_composition_(computer_science)) 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: {% post kzagoris/introducing-alpinejs-the-ideal-jquery-replacement-jfa %}

## Architecture

Let's have as a base example the Netlify Contact Form described in the previous article: {% link kzagoris/developing-a-netlify-contact-form-using-alpine-js-3i95 %}

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jykm8ljythyqk3xxg3ts.png)

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:

```js
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;
}
```

## netlifySubmission Module

The `netlifySubmission` module handles the backend submission to the [netlify service](https://www.netlify.com/products/forms/). 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:

```js
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));
    },
  };
}
```

## 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:

```html
<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>
```

And it produces the following behavior:

![Form Validation Exmple](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jc8j00srhl5fdk6xqk5w.gif)

The full code for the `formValidator` module is:

```js
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}`)
      );
    },
  };
}
```

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](https://github.com/alpinejs/alpine#x-data) attribute expects and the browser's [native module support](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules).

## 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](https://github.com/alpinejs/alpine#x-init) attribute and the [$watch](https://github.com/alpinejs/alpine#watch) magic attribute may be of help. We write inside the HTML file that the form lies the following code:

```js
 <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>
```

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules).

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](https://github.com/alpinejs/alpine#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](https://github.com/alpinejs/alpine#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:

{% codesandbox alpinejs-function-composition-lpjqd %}