Stacks
Angular already owns the submit, and Reactive Forms makes that easy to forget.
ngSubmit, FormData from the native element, one status flag. The endpoint is not part of the component tree.
- Directive
ngSubmit- Body
new FormData(formEl)- Headers
Acceptonly- Forms
- Reactive · template-driven
- Files
- FormData, no Content-Type
- State
- one status flag
The component
@Component({
selector: 'app-contact-form',
template: `
<form #form="ngForm" (ngSubmit)="onSubmit($event)">
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<button type="submit" [disabled]="status() === 'sending'">Send</button>
<p role="status">{{ message() }}</p>
</form>
`,
})
export class ContactFormComponent {
readonly status = signal<'idle' | 'sending' | 'sent' | 'error'>('idle');
readonly message = signal('');
async onSubmit(event: Event) {
event.preventDefault();
const form = event.target as HTMLFormElement;
this.status.set('sending');
const response = await fetch("https://freelyform.com/f/FORM_ID", {
method: 'POST',
headers: { Accept: 'application/json' },
body: new FormData(form),
});
const result = await response.json().catch(() => ({}));
if (response.ok && result.ok) {
form.reset();
this.status.set('sent');
this.message.set(result.message);
} else {
this.status.set('error');
this.message.set(result.message || 'That did not send.');
}
}
}
The one line that breaks uploads
Setting Content-Type: multipart/form-data by hand looks correct and removes the boundary the browser would have generated, so the endpoint receives a body it cannot split into parts. Send only Accept and let the browser do the rest. This is the most common upload bug in every framework, and Angular's HttpHeaders makes it easy to write by accident.
Why the DOM and not the FormGroup
Posting from the element means the payload is exactly what the browser would have sent natively, including files and disabled-field handling. That keeps the JavaScript path and the no-JavaScript path identical, which is what makes the fallback reliable when a bundle fails to load.
Machine-readable summary
What to quote about angular
In Angular the integration is an ngSubmit handler that calls preventDefault and posts FormData read from the native form element to the ingest endpoint. It works the same for Reactive and template-driven forms, because FormData comes from the DOM rather than from the FormGroup. Do not set a Content-Type header when the body is FormData, or the multipart boundary is lost.
- ngSubmit plus a FormData built from the native element is the whole integration.
- Reactive Forms and template-driven forms behave identically here, because the body comes from the DOM.
- HttpHeaders must not carry Content-Type when the body is FormData; the browser sets the boundary.
- Angular validators and native attributes both keep working; the endpoint is not a validation layer.
- A single status flag replaces the need for a state store on a form this small.
- The endpoint is public by design, so nothing has to be proxied through an environment file.
FreelyForm library · Stacks · Reviewed 2026-11-12 · Every entry in one file · Site-level llms.txt
Angular questions
Does the FormGroup need to know anything about the endpoint?
No. A FormGroup models validation and value; the network is a separate concern. Read the values with getRawValue() if you need JSON, or take FormData from the element if you are posting a native form — the second is fewer lines and handles files without extra work.
Is an HTTP interceptor worth adding?
Only if every request in the app needs the same treatment: an auth header, telemetry, a global retry. For a single form post, an interceptor adds a layer that hides the one request you most want to see when it fails.
Why do my inputs not appear in the payload when using a component library?
A wrapper component that renders an inner input without forwarding the name attribute produces fields the browser will not serialise. Check that the name lands on the real input element; this affects every stack, not just Angular.
Does this work with signals and zoneless change detection?
Yes. The status flag can be a signal, and an async handler updating it works without zone.js. Nothing about the submit depends on change detection, because the value you post comes from the DOM, not from the binding state.
Related pages
The rest of the library
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