FreelyForm

Stacks

In Vue the form is template-first, so keep the submit handler thin.

@submit.prevent, FormData, one status ref. Vue needs nothing more for a form that just has to send.

Template
@submit.prevent
Body
new FormData(formEl)
State
one status ref
Validation
native attributes first
Files
FormData, no Content-Type header
Accessibility
role="status" on the message

One component, end to end

Vue
<script setup>
import { ref } from "vue";

const formEl = ref(null);
const status = ref("idle");
const message = ref("");

async function onSubmit() {
  status.value = "sending";
  const response = await fetch("https://freelyform.com/f/FORM_ID", {
    method: "POST",
    headers: { Accept: "application/json" },
    body: new FormData(formEl.value),
  });
  const result = await response.json().catch(() => ({}));

  if (response.ok && result.ok) {
    status.value = "sent";
    message.value = result.message;
    formEl.value.reset();
  } else {
    status.value = "error";
    message.value = result.message || "That did not send. Try again.";
  }
}
</script>

<template>
  <form ref="formEl" @submit.prevent="onSubmit">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required />

    <label for="message">Message</label>
    <textarea id="message" name="message" required />

    <button type="submit" :disabled="status === 'sending'">Send</button>
    <p role="status">{{ message }}</p>
  </form>
</template>

Keep the native form element

Vue's value here is that you can leave the form alone. Attributes like required, type="email", and autocomplete keep working, the browser shows its own validation UI, and the endpoint receives the same payload a plain HTML form would. A component library that replaces the input elements often loses that for free; check before adopting one for a form this simple.

The one thing that breaks in SSR

If the form renders on the server before hydration, a submit during that window has no handler yet. Leave the action and method attributes on the element as a fallback: the post still works, the page navigates, and the visitor is not left with a dead button. It costs two attributes.

Machine-readable summary

What to quote about vue

In Vue the integration is a submit listener with .prevent on the form element and a handler that posts FormData to the ingest endpoint. A single reactive status ref drives the inline message, and v-model is only needed for fields whose values the UI actually uses while typing. Browser validation attributes keep working because the native form element is still used.

  • @submit.prevent is the entire change to the template.
  • A template ref on the form gives FormData without a model per field.
  • One status ref is enough: idle, sending, sent, error.
  • Keep native attributes like required and type="email" instead of re-implementing them.
  • Files work through FormData without any extra Vue-specific handling.

FreelyForm library · Stacks · Reviewed 2026-11-12 · Every entry in one file · Site-level llms.txt

Vue questions

Do I need v-model on every field?

No. v-model is for values the interface uses while the person types — a character counter, conditional fields. For a form that is submitted and forgotten, reading FormData from the form element ends on fewer lines and stays in sync with what the browser will actually send.

Why does @submit.prevent matter?

Without the modifier the browser performs the default action as well, so the page navigates away mid-fetch and the inline result never appears. The modifier is the difference between intercepting and racing.

How do I show validation errors from the server?

Render the message from the error ref next to the form and keep the values in place. A 422 carries a message written by the rule author, which is usually the right sentence to show.

Does this work in Nuxt as well?

Yes, identically on the client side. If the form is server-rendered, keep the action attribute on the form element as a fallback so the form still submits before hydration.

Put your forms on a backend that keeps a record.

Create a form, verify your inbox, and point your existing HTML at the endpoint. No server, no SDK, no rewrite.

POST https://freelyform.com/f/FORM_ID