October 5, 2026 · NBForms Team
Angular Contact Form: Two Official Ways, and the One That Isn't
Angular ships two different ways to build a form, and both lead to the same HttpClient gotcha before a contact form reaches NBForms. A third option skips the gotcha entirely.
Angular doesn't give a form just one way to be built. Reactive Forms and template-driven forms are both first-class, both fully supported, and both show up in different tutorials depending on which one the author reached for — which means "how do I post an Angular contact form to NBForms" doesn't have one answer so much as two, plus a handful of Angular-specific details that trip up either path the same way.
Photo by Stephen Craven, CC BY-SA 2.0, via Wikimedia Commons
The version that barely looks like Angular
Template-driven forms keep almost everything in the HTML, with ngModel doing the wiring and a
template reference variable handing the whole form's value to the class on submit:
<!-- contact.component.html -->
<form #contactForm="ngForm" (ngSubmit)="onSubmit(contactForm.value)">
<label>Name</label>
<input type="text" name="name" ngModel required />
<label>Email</label>
<input type="email" name="email" ngModel required />
<label>Message</label>
<textarea name="message" ngModel required></textarea>
<button type="submit">Send</button>
</form>
// contact.component.ts
@Component({ standalone: true, imports: [FormsModule], templateUrl: './contact.component.html' })
export class ContactComponent {
private http = inject(HttpClient)
private token = 'YOUR_TOKEN'
onSubmit(value: Record<string, string>) {
this.http.post('https://api.nbforms.com', { ...value, _token: this.token }).subscribe()
}
}
The version where the form lives in the class
Reactive Forms moves the same information into the component class instead, as an explicit
FormGroup built from named FormControls, with the template binding to that object rather than
owning the state itself:
// contact.component.ts
@Component({ standalone: true, imports: [ReactiveFormsModule], templateUrl: './contact.component.html' })
export class ContactComponent {
private http = inject(HttpClient)
form = new FormGroup({
name: new FormControl('', Validators.required),
email: new FormControl('', Validators.required),
message: new FormControl('', Validators.required),
})
onSubmit() {
this.http.post('https://api.nbforms.com', { ...this.form.value, _token: 'YOUR_TOKEN' }).subscribe()
}
}
<!-- contact.component.html -->
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<label>Name</label>
<input type="text" formControlName="name" />
<label>Email</label>
<input type="email" formControlName="email" />
<label>Message</label>
<textarea formControlName="message"></textarea>
<button type="submit">Send</button>
</form>
The request that never leaves if nothing subscribes
Both versions above end the same way: a call to HttpClient.post(). That call alone does
nothing — HttpClient's methods return cold Observables, which don't execute until something
subscribes to them. Skip the .subscribe() and the component compiles cleanly, the button click
handler runs without error, and no request ever reaches NBForms, because nothing ever asked the
Observable to actually do the work it was built to do. This isn't specific to forms — it's the
same rule for every HttpClient call in Angular — but a submit handler is exactly the kind of
place a bare .post(...) with the .subscribe() left off goes unnoticed the longest, since the
rest of the UI gives no sign anything was supposed to happen. .subscribe() itself doesn't have
to be the final word on it — piping the call through firstValueFrom() from rxjs turns it into
a Promise an async/await submit handler can sit on instead, which subscribes internally on
its own — but one of the two has to happen somewhere, by hand or otherwise, or the Observable just
sits there, built and unused.
The value that disappears the moment a control is disabled
Reactive Forms has a second trap, specific to itself: FormGroup.value leaves out any control
that's currently disabled. Disable a field — to grey it out conditionally, say, or lock it after
a first submit — and this.form.value silently stops including it, with no warning that the
payload just got smaller. This isn't a bug so much as Angular matching native behavior on
purpose: a disabled native HTML input has never been included in a form submission either.
this.form.getRawValue() is the explicit opt-in for "include it anyway," and it's the one to
reach for whenever a disabled control still needs to reach NBForms.
What neither template loads without
Both versions above assume HttpClient is available to inject in the first place, which takes
one line of setup that's easy to do once and forget exists: provideHttpClient() in the app's
bootstrap configuration for a standalone app (or HttpClientModule imported into an NgModule
on an older project). Skip it, and injecting HttpClient fails at startup, before either form
pattern above even gets a chance to matter. The two form APIs also each need their own module in
a component's imports array — FormsModule for ngModel, ReactiveFormsModule for
formGroup/formControlName — and mixing the two up (importing one while writing markup for the
other) produces template errors that point at the symptom, not this cause.
The header that was never going to attach anyway
One thing that doesn't need worrying about either way: Angular's built-in CSRF protection
automatically reads an XSRF-TOKEN cookie and attaches it as an X-XSRF-TOKEN header, but only
on same-origin requests. A POST to https://api.nbforms.com from an app hosted anywhere else is
cross-origin by definition, so that interceptor stays out of it entirely — nothing to disable,
nothing extra arriving in the request.
The form that needed neither API
Here's what both versions above were building toward, arrived at directly: a plain HTML form, with no Angular form directive anywhere in it, works unmodified inside an Angular template.
<!-- contact.component.html -->
<form action="https://api.nbforms.com" method="POST">
<input type="hidden" name="_token" value="YOUR_TOKEN" />
<label>Name</label>
<input type="text" name="name" required />
<label>Email</label>
<input type="email" name="email" required />
<label>Message</label>
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>
No FormsModule, no ReactiveFormsModule, no injected HttpClient, no .subscribe() to
remember, no disabled-control gotcha because there's no FormGroup to disable anything on. The
browser POSTs it the same way it would on a page with no framework at all.
Picking one, honestly
The plain version isn't a universal upgrade over the other two — it gives up the inline,
no-navigation success state a single-page Angular app usually wants, since a native form
submission is a real page load. That's exactly what HttpClient plus (ngSubmit) buys back, and
it's a reasonable thing to want. Between the two Angular-native options, Reactive Forms earns its
extra ceremony on a form with real validation logic or fields that need to react to each other;
template-driven forms fit a form this simple just as well with less code. All three reach the
same place — what changes is only how much Angular machinery sits between a click and NBForms
actually receiving it.
For the same pattern in other frameworks, see the framework-specific snippets; for turning this same form into a page with nothing else around it, see hosted form pages.
Frequently asked questions
Does a contact form this size need Reactive Forms or NgModel at all?
No — both are Angular's answer to tracking form state in the class for validation, dynamic fields, or reading values before submit. A form that only needs to collect values at the moment someone clicks submit doesn't need either; a plain HTML form does the same job with no Angular form API involved.
Why doesn't anything happen when HttpClient.post() is called?
Because HttpClient methods return cold Observables — lazy by design, meaning the HTTP request itself doesn't fire until something calls .subscribe() on it. Calling .post() alone builds the request; it doesn't send it.
Why is a field missing from the data a Reactive Form submits?
Check whether that control is disabled. FormGroup.value deliberately excludes disabled controls from the aggregate value — the same way a disabled native HTML input is excluded from a plain form submission. form.getRawValue() includes it if that's not what's wanted.
Do I need HttpClientModule imported somewhere?
In a standalone Angular app (the current default), call provideHttpClient() in the app's bootstrap config instead; older NgModule-based apps still import HttpClientModule. Either way, HttpClient has to be explicitly provided before it can be injected anywhere.
Will Angular's built-in CSRF protection add a header NBForms doesn't expect?
No — Angular's XSRF interceptor only attaches its X-XSRF-TOKEN header to same-origin requests. A POST to an external endpoint like NBForms is cross-origin by definition, so the interceptor doesn't touch it either way.