Building an AJAX form with petite-vue

Bjorn Krolsavatar

Bjorn Krols

Published on
22 November 2021

In this article you will learn how to build a form with petite-vue that submits data via AJAX.

petite-vue is an alternative distribution of Vue optimized for progressive enhancement. It provides the same template syntax and reactivity mental model with standard Vue. However, it is specifically optimized for "sprinkling" small amount of interactions on an existing HTML page rendered by a server framework.

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 petite-vue magic. Add the following script tag inside head.

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="//unpkg.com/petite-vue" defer init></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".

v-scope marks a chunk of HTML that should be controlled by petite-vue. A scope can store properties and methods.

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

<body>
  <form v-scope="ContactForm()">
    <!-- ... -->
  </form>
  <script>
    function ContactForm() {
      return {};
    }
  </script>
</body>

To set up input reactivity, add a formData object to the scope (you can pick a different name) and link its properties to inputs via v-model.

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

<body>
  <form v-scope="ContactForm()">
    <div>
      <label>Name:</label>
      <input type="text" name="name" required v-model="formData.name" />
    </div>
    <div>
      <label>Email:</label>
      <input type="email" name="email" required v-model="formData.email" />
    </div>
    <div>
      <label>Message:</label>
      <textarea name="message" required v-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 v-scope="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 v-scope="ContactForm()" @submit.prevent="submitForm">
    <!-- ... -->
    <button>Submit</button>
    <div>{{ 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 you can use "Mustache" syntax to access properties from the scope in your HTML.

<div>{{ 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 v-scope="ContactForm()" @submit.prevent="submitForm">
    <!-- ... -->
    <button :disabled="formLoading">{{ 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">{{ buttonText }}</button>

That's it. You now know how to create your own contact form with petite-vue 🎉.

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