Building an AJAX form with Alpine.js

Bjorn Krolsavatar

Bjorn Krols

Published on
13 December 2021

In this article you will learn how to build a form with Alpine.js that submits data via AJAX.

Alpine.js is a rugged, minimal framework for composing JavaScript behavior in your markup.

This article is compatible with Alpine.js V3.

Start with the following HTML of a minimal contact form.

<!DOCTYPE html>
<html lang="en">
  <head></head>
  <body>
    <form>
      <div>
        <label>Name:</label>
        <input type="text" name="name" required />
      </div>
      <div>
        <label>Email:</label>
        <input type="email" name="email" required />
      </div>
      <div>
        <label>Message:</label>
        <textarea name="message" required></textarea>
      </div>
      <button>Submit</button>
    </form>
  </body>
</html>

Let's enhance it with some Alpine.js magic. Add the following script tag inside head.

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="//unpkg.com/alpinejs" defer></script>
  </head>
  <body>
    <!-- ... -->
  </body>
</html>
  • The defer attribute makes the script execute after the page has finished parsing.
  • The two forward slashes are a common shorthand for "whatever protocol is being used right now".

x-data marks a chunk of HTML that should be controlled by Alpine.js. A scope can store properties, methods, and getters.

Create a function returning an empty scope and attach it to the form with x-data.

<body>
  <form x-data="contactForm()">
    <!-- ... -->
  </form>
  <script>
    function ContactForm() {
      return {};
    }
  </script>
</body>

To set up input reactivity, add a formData object to the scope and link its properties to inputs via x-model.

As you type in the input element, the property values are automagically updated 🧙‍♂.

<body>
  <form x-data="ContactForm()">
    <div>
      <label>Name:</label>
      <input type="text" name="name" required x-model="formData.name" />
    </div>
    <div>
      <label>Email:</label>
      <input type="email" name="email" required x-model="formData.email" />
    </div>
    <div>
      <label>Message:</label>
      <textarea name="message" required x-model="formData.message"></textarea>
    </div>
    <button>Submit</button>
  </form>
  <script>
    function ContactForm() {
      return {
        formData: {
          name: "",
          email: "",
          message: "",
        },
      };
    }
  </script>
</body>

The form element emits a submit event you can listen to using the @submit directive.

Start by registering a simple submit listener that logs the form data.

Add the .prevent modifier to prevent the browser from submitting a native form request.

<body>
  <form x-data="ContactForm()" @submit.prevent="submitForm">
    <!-- ... -->
    <button>Submit</button>
  </form>
  <script>
    function ContactForm() {
      return {
        formData: {
          // ...
        },
        submitForm() {
          console.log(JSON.stringify(this.formData));
        },
      };
    }
  </script>
</body>

Let's upgrade the submit listener to actually send the data with fetch.

⚡ Looking for an easy-to-setup form backend? Check out Formspark.

The POST method is commonly used to send data (enclosed in the request's body) to a server to create or update a resource.

Use JSON.stringify to serialize the data into a string.

The Content-Type header indicates the media type of the content you're sending.

The Accept header indicates what kind of content you're expecting in response.

When the AJAX call finishes we want to:

  • Reset the form values if the submission was successful.
  • Show a message indicating whether the submission was successful.
<body>
  <form x-data="ContactForm()" @submit.prevent="submitForm">
    <!-- ... -->
    <button>Submit</button>
    <div x-text="formMessage"></div>
  </form>
  <script>
    const FORM_URL = "https://submit-form.com/technotrampoline";
    function ContactForm() {
      return {
        formData: {
          // ...
        },
        formMessage: "",
        submitForm() {
          this.formMessage = "";
          fetch(FORM_URL, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              Accept: "application/json",
            },
            body: JSON.stringify(this.formData),
          })
            .then(() => {
              this.formData.name = "";
              this.formData.email = "";
              this.formData.message = "";
              this.formMessage = "Form successfully submitted.";
            })
            .catch(() => {
              this.formMessage = "Something went wrong.";
            });
        },
      };
    }
  </script>
</body>

Note how x-text binds the text value of an element to the value of a scope property.

<div x-text="formMessage"></div>

To improve the loading user-experience we want to:

  • Disable the submit button while it's submitting.
  • Change the content of the submit button while it's submitting.
<body>
  <form x-data="ContactForm()" @submit.prevent="submitForm">
    <!-- ... -->
    <button :disabled="formLoading" x-text="buttonText"></button>
    <!-- ... -->
  </form>
  <script>
    // ...
    function ContactForm() {
      return {
        formData: {
          name: "",
          email: "",
          message: "",
        },
        formMessage: "",
        formLoading: false,
        buttonText: "Submit",
        submitForm() {
          this.formMessage = "";
          this.formLoading = false;
          this.buttonText = "Submitting...";
          fetch(FORM_URL, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              Accept: "application/json",
            },
            body: JSON.stringify(this.formData),
          })
            .then(() => {
              this.formData.name = "";
              this.formData.email = "";
              this.formData.message = "";
              this.formMessage = "Form successfully submitted.";
            })
            .catch(() => {
              this.formMessage = "Something went wrong.";
            })
            .finally(() => {
              this.formLoading = false;
              this.buttonText = "Submit";
            });
        },
      };
    }
  </script>
</body>

Note how :disabled binds the value of an attribute to the value of a scope property.

<button :disabled="formLoading" x-text="buttonText"></button>

That's it. You now know how to create your own contact form with Alpine.js 🎉.

Thank you for reading through, feel free to leave a comment below.

Subscribe to our newsletter

The latest news, articles, and resources, sent to your inbox weekly.

More like this