Home / Articles / Developing AlpineJs Apps Similar to Vue's Composition API

Developing AlpineJs Apps Similar to Vue's Composition API

December 30, 2020
7 min. read

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.

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:

 1export function isEmail(value) {
 2  return new RegExp("^\\S+@\\S+[\\.][0-9a-z]+$").test(
 3    String(value).toLowerCase()
 4  );
 5}
 6
 7export function isRequired(value) {
 8    return value !== null && value !== undefined && value.length > 0;
 9}
10
11export function hasMinLength(value, length) {
12  return String(value).length >= length;
13}
14
15export function hasMaxLength(value, length) {
16  return String(value).length <= length;
17}

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:

 1export function netlifySubmission() {
 2  return {
 3    submitToNetlify(formElement) {
 4      let body = new URLSearchParams(new FormData(formElement)).toString();
 5      return fetch("/", {
 6        method: "POST",
 7        headers: {
 8          "Content-Type": "application/x-www-form-urlencoded",
 9        },
10        body: body,
11      })
12        .then((response) => {
13          if (response.ok) {
14            formElement.reset();
15            alert("Thank you for your message!");
16          } else {
17            throw new Error(`Something went wrong: ${response.statusText}`);
18          }
19        })
20        .catch((error) => console.error(error));
21    },
22  };
23}

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:

1<p>
2        <label
3          >Full Name: <input x-model="name" x-on:blur="nameDirty = true" type="text" name="name"
4        /></label>
5      </p>
6      <p x-show.transition="!isNameValid() && nameDirty" style="color: red" x-cloak>
7        Please fill out your full name.
8      </p>

And it produces the following behavior:

The full code for the formValidator module is:

 1import {
 2  isEmail,
 3  hasMaxLength,
 4  hasMinLength,
 5  isRequired,
 6} from "./validators.mjs";
 7
 8export function formValidator() {
 9  let submitBackend;
10  return {
11    name: null,
12    nameDirty: false,
13    email: null,
14    emailDirty: false,
15    comments: null,
16    commentsDirty: false,
17    isNameValid(maxLength) {
18      return (
19        this.nameDirty &&
20        isRequired(this.name) &&
21        (maxLength ? hasMaxLength(this.name, maxLength) : true)
22      );
23    },
24    isEmailValid() {
25      return this.emailDirty && isEmail(this.email);
26    },
27    isCommentsValid(minLength) {
28      return (
29        this.commentsDirty &&
30        isRequired(this.comments) &&
31        (minLength ? hasMinLength(this.comments, minLength) : true)
32      );
33    },
34    isFormValid() {
35      return (
36        this.isNameValid() && this.isEmailValid() && this.isCommentsValid()
37      );
38    },
39    submitForm() {
40      this.nameDirty = true;
41      this.emailDirty = true;
42      this.commentsDirty = true;
43      if (!this.isFormValid()) return;
44      submitBackend(this.$el);
45    },
46    formValidator_init(backend) {
47      submitBackend = backend;
48      this.$watch("name", (value) =>
49        console.log(`The name change to ${value}`)
50      );
51    },
52  };
53}

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:

 1 <script type="module" defer>
 2      import { formValidator } from "./form-validator.mjs"
 3      import { netlifySubmission } from "./netlify.mjs"
 4
 5      window.contactForm = function() {
 6         return {
 7           ...netlifySubmission(),
 8           ...formValidator(),
 9          init() {
10             this.formValidator_init(this.submitToNetlify);
11             this.$watch("email", (value) =>
12               console.log(`the value of the email is ${this.email}`)
13             );
14           }
15         }
16       }
17    </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.

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:

Share:

comments powered by Disqus

Also Read:

One of the most common web app patterns involves collecting data from a form and submitting it to a REST API or, the opposite, populating a form from data originating from a REST API. This pattern can easily be achieved in Alpinejs using the native javascript Fetch Api. As a bonus, I describe the fetch async version at the end of the article.
One of the most frequent requirements when writing AlpineJs components is the communication between them. There are various strategies for how to tackle this problem. This article describes the four most common patterns that help pass information between different Alpinejs components.
Most uncomplicated today web sites, like this blog, for example, or a landing page, do not need extensive javascript frameworks with complicated build steps. The Alpine.js framework is a drop-in library that replaces the jQuery query-based (imperative) approach with the tailwindcss-inspired declarative approach on the DOM using a familiar Vue-like syntax.