Back to blog

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.

Railway tracks converging into one switch near a row of apartment buildings 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.