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.