Skip to content
Nick  Cotton

Using Alpine.js with Netlify Forms

I recently added a contact form to the home page of my site. Given that I was already using Alpine.js and Netlify, I decided to try and hook it up using the Netlify Forms feature. I ran into a couple of issues which I found confusing and poorly documented. This post is a helping hand to my future self.

Netlify Forms works by scanning your deploy for any forms and then added a hidden input field with the form name.

So a form authored like this:

<form
  name="contact"
  method="POST"
  data-netlify="true">
  <p>
    <label for="name">Your name</label>
    <input id="name" name="name" type="text" />
  </p>
  <p>
    <label for="email">Your email</label>
    <input id="email" name="email" type="email" placeholder="test@example.com" />
  </p>
  <p class="font-sans text-xl col-span-2">
    <label for="message">Message</label>
    <textarea name="message"></textarea>
  </p>
</form>

Would be transformed to look like this:

<form
  name="contact"
  method="POST"
  data-netlify="true">
  <input type="hidden" name="form-name" value="contact">
  <p>
    <label for="name">Your name</label>
    <input id="name" name="name" type="text" />
  </p>
  <p>
    <label for="email">Your email</label>
    <input id="email" name="email" type="email" placeholder="test@example.com" />
  </p>
  <p class="font-sans text-xl col-span-2">
    <label for="message">Message</label>
    <textarea name="message"></textarea>
  </p>
  <button type="submit"></button>
</form>

With that working, I started to add in various attributes to collect the data and handle the form submission using Alpine, rather than a normal form submission. So my form now looks like this:

<form
  name="contact"
  method="POST"
  data-netlify="true"
  @submit.prevent="submitForm">
  <p>
    <label for="name">Your name</label>
    <input x-model="formData.name" id="name" name="name" type="text" />
  </p>
  <p>
    <label for="email">Your email</label>
    <input x-model="formData.email" id="email" name="email" type="email" placeholder="test@example.com" />
  </p>
  <p class="font-sans text-xl col-span-2">
    <label for="message">Message</label>
    <textarea x-model="formData.message" name="message"></textarea>
  </p>
  <button type="submit"></button>
</form>

Here's the relevant snippet from the JS:

function ContactForm() {
  return {
    formData: {
      name: "",
      email: "",
      message: "",
    },
    submitForm() {
      this.buttonState = "loading";

      const formData = new FormData();
      Object.keys(this.formData).forEach(key => {
        formData.append(key, this.formData[key]);
      });
      fetch(location.href, {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: new URLSearchParams(formData).toString(),
      })
        .then(() => {
          this.buttonState = "success";
          this.formData.name = "";
          this.formData.email = "";
          this.formData.message = "";
        })
        .catch((e) => {
          this.buttonState = "error";
          console.error(e);
        })
        .finally(() => {
          setTimeout(() => {
            this.buttonState = "ready";
          }, 3000);
        })
    }
  }
}

Hmm, it's stopped working. When I submit the form now, the JS takes over but I get a 404 response in the devtools network tab.

After a bit of digging, I discovered that the service that manipulates the form on the Netlify side just bails out if it hits an invalid character (like an @).

So what if we just manually add the value of that hidden form field to the formData object that we send over to Netlify.

function ContactForm() {
  return {
    formData: {
      name: "",
      email: "",
      message: "",
+     "form-name": "contact",
    }
  }
}

Bingo! It works again.

I never discovered a great way to test this locally. It involved a lot of preview deployments and a few panicked pushed directly to main. If anyone does know how to do that, I'd love to hear!