Formspree Logo
Guide Thumbnail
+

Forms in Angular

In this guide we’ll show you how to add a contact form to your Angular website using Formspree.

Formspree is a form backend that’s an ideal companion for front-end frameworks like Angular, letting you focus on your application’s UI without needing to set up a server to handle forms

At the end of this guide, you’ll have a working contact form in your Angular site that sends you email notifications using Formspree.

Prerequisites

To follow this guide, you’ll need:

If you don’t already have an Angular project, you can create one with the Angular CLI:

npx @angular/cli new angular-formspree-starter
cd angular-formspree-starter
ng serve

Angular supports reactive forms and template-driven forms to give you more control over how your forms behave. Formspree will work with any of those as long as you’re sending the form data to the Formspree form endpoint. In this guide, you will learn how to implement Angular forms through both of these methods.

Both Reactive Forms and Template-Driven Forms methods will post to a Formspree endpoint, but they offer different levels of control over form validation and behavior.

You can find the complete code from this guide in this GitHub repo.

Step 1: Add the Form Component

First, create a new component for your form:

ng generate component contact-form

For both reactive and template-driven approaches, you will need to add some CSS to contact-form.component.css to style your forms. You can get the CSS here.

Now, let’s look at the options one by one.

Option 1: Reactive Forms

Reactive Forms in Angular are a model-driven approach to building forms where form controls are created and managed programmatically in the component class.

It makes sense to use them when you need dynamic, programmatically-driven form control with complex validation logic, conditional rendering, or runtime form generation. Reactive Forms offer better scalability and testability compared to Template-driven Forms because the form structure and behavior are defined entirely in the component class

To implement a reactive form, update the component files:

contact-form.component.ts:

import { Component, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { HttpClient, HttpClientModule } from '@angular/common/http';

@Component({
  selector: 'app-contact-form',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule, HttpClientModule],
  templateUrl: './contact-form.component.html',
  styleUrls: ['./contact-form.component.css'],
  encapsulation: ViewEncapsulation.None
})
export class ContactFormComponent {
  form: FormGroup;
  submitting = false;
  submitted = false;
  error: string | null = null;

  constructor(private fb: FormBuilder, private http: HttpClient) {
    this.form = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      message: ['']
    });
  }

  onSubmit() {
    if (this.form.invalid) {
      this.form.markAllAsTouched();
      return;
    }
    this.submitting = true;
    this.error = null;
    this.http.post('https://formspree.io/f/{FORM_ID}', this.form.value, {
      headers: { 'Accept': 'application/json' }
    }).subscribe({
      next: () => {
        this.submitting = false;
        this.submitted = true;
        this.form.reset();
      },
      error: (err) => {
        this.submitting = false;
        this.error = 'Submission failed. Please try again.';
      }
    });
  }
}

This Angular component defines a standalone reactive contact form using FormBuilder to create a FormGroup with email and message fields, applies validation to the email, and submits the form data via HttpClient to a Formspree endpoint. It tracks form state with flags like submitting and submitted, handles both success and error cases, and uses Angular modules like ReactiveFormsModule and HttpClientModule to enable form handling and HTTP requests.

contact-form.component.html:

<form [formGroup]="form" class="fs-form" (ngSubmit)="onSubmit()" novalidate>
  <div class="fs-field">
    <label class="fs-label" for="email">Email</label>
    <input 
      class="fs-input" 
      id="email" 
      name="email" 
      formControlName="email" 
      required 
      [class.ng-invalid]="form.get('email')?.invalid && (form.get('email')?.touched || form.get('email')?.dirty)"
    />
    <div *ngIf="form.get('email')?.invalid && (form.get('email')?.touched || form.get('email')?.dirty)" class="fs-description" style="color: #e53e3e;">
      <span *ngIf="form.get('email')?.errors?.['required']">Email is required.</span>
      <span *ngIf="form.get('email')?.errors?.['email']">Please enter a valid email.</span>
    </div>
  </div>
  <div class="fs-field">
    <label class="fs-label" for="message">Message</label>
    <textarea class="fs-textarea" id="message" name="message" formControlName="message"></textarea>
  </div>
  <div class="fs-button-group">
    <button class="fs-button" type="submit" [disabled]="form.invalid || submitting">
      {{ submitting ? 'Submitting...' : 'Submit' }}
    </button>
  </div>
  <div *ngIf="submitted" class="fs-description" style="color: #16a34a; margin-top: 1rem;">
    Thank you! Your submission has been received.
  </div>
  <div *ngIf="error" class="fs-description" style="color: #e53e3e; margin-top: 1rem;">
    {{ error }}
  </div>
</form>

This HTML template binds the reactive form to the DOM using [formGroup] on the <form> element and formControlName on each input, which connects each field to its corresponding control defined in the component class. It uses *ngIf to display real-time validation messages only when the user has interacted with the email field and it’s invalid.

The form also conditionally disables the submit button if the form is invalid or submitting, and shows success or error messages after submission. formGroup binds the entire form, formControlName links inputs to form controls, and Angular’s built-in validation states like touched, dirty, and errors provide real-time user feedback.

Option 2: Template-Driven Forms

Template-driven forms in Angular are forms where the form logic, structure, and validation are primarily defined in the HTML template using directives like ngModel, with minimal code in the component class.

These are ideal for simple forms with straightforward validation needs, such as basic contact forms or signup pages, where you want to rely heavily on Angular’s declarative syntax in the HTML template rather than managing form logic in the component class. They require less boilerplate and are easier to set up for smaller projects or forms with limited interactivity, making them a good choice when you don’t need dynamic form control creation, advanced validation logic, or tight programmatic control over form behavior.

Update the component files:

contact-form.component.ts:

import { Component, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HttpClient, HttpClientModule } from '@angular/common/http';

@Component({
  selector: 'app-contact-form',
  standalone: true,
  imports: [CommonModule, FormsModule, HttpClientModule],
  templateUrl: './contact-form.component.html',
  styleUrls: ['./contact-form.component.css'],
  encapsulation: ViewEncapsulation.None
})
export class ContactFormComponent {
  email = '';
  message = '';
  submitting = false;
  submitted = false;
  error: string | null = null;

  constructor(private http: HttpClient) {}

  onSubmit() {
    if (!this.email) {
      return;
    }
    
    this.submitting = true;
    this.error = null;
    
    this.http.post('https://formspree.io/f/{FORM_ID}', {
      email: this.email,
      message: this.message
    }, {
      headers: { 'Accept': 'application/json' }
    }).subscribe({
      next: () => {
        this.submitting = false;
        this.submitted = true;
        this.email = '';
        this.message = '';
      },
      error: (err) => {
        this.submitting = false;
        this.error = 'Submission failed. Please try again.';
      }
    });
  }
}

This component uses the template-driven approach by defining form data (email and message) as component properties, which are bound to form inputs in the template using [(ngModel)]. It submits the form data using Angular’s HttpClient to a Formspree endpoint, and manages submission state with flags like submitting, submitted, and error. Unlike reactive forms, there’s no FormGroup or explicit form validation logic in the component; instead, validation is expected to be handled in the HTML template using Angular directives like required.

contact-form.component.html:

<form #contactForm="ngForm" class="fs-form" (ngSubmit)="onSubmit()" novalidate>
  <div class="fs-field">
    <label class="fs-label" for="email">Email</label>
    <input 
      class="fs-input" 
      id="email" 
      name="email" 
      [(ngModel)]="email" 
      #emailInput="ngModel"
      required 
      email
      [class.ng-invalid]="emailInput.invalid && (emailInput.touched || emailInput.dirty)"
    />
    <div *ngIf="emailInput.invalid && (emailInput.touched || emailInput.dirty)" class="fs-description" style="color: #e53e3e;">
      <span *ngIf="emailInput.errors?.['required']">Email is required.</span>
      <span *ngIf="emailInput.errors?.['email']">Please enter a valid email.</span>
    </div>
  </div>
  <div class="fs-field">
    <label class="fs-label" for="message">Message</label>
    <textarea 
      class="fs-textarea" 
      id="message" 
      name="message" 
      [(ngModel)]="message"
    ></textarea>
  </div>
  <div class="fs-button-group">
    <button 
      class="fs-button" 
      type="submit" 
      [disabled]="contactForm.invalid || submitting"
    >
      {{ submitting ? 'Submitting...' : 'Submit' }}
    </button>
  </div>
  <div *ngIf="submitted" class="fs-description" style="color: #16a34a; margin-top: 1rem;">
    Thank you! Your submission has been received.
  </div>
  <div *ngIf="error" class="fs-description" style="color: #e53e3e; margin-top: 1rem;">
    {{ error }}
  </div>
</form>

This template uses Angular’s template-driven form approach, where each input is bound to a component property using [(ngModel)] as mentioned earlier, and the form itself is tracked using the #contactForm="ngForm" template reference. Validation is handled directly in the template using directives like required and email, with visual feedback shown conditionally using *ngIf and Angular’s form state properties like touched, dirty, and invalid. Beginners should note that ngModel is the key directive linking the form input to the component, and form state (like validity) is accessible through local references such as #emailInput="ngModel" for validation display.

Once you have implemented one of these two approaches, add the component to your app:

<app-contact-form></app-contact-form>

If you’re using a non-standalone component, make sure to declare it in your module:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ContactFormComponent } from './contact-form/contact-form.component';

@NgModule({
  declarations: [ContactFormComponent],
  imports: [BrowserModule],
  bootstrap: [AppComponent]
})
export class AppModule { }

Step 2: Creating a Form Endpoint

Next, you need to create a form endpoint using Formspree. If you don’t have an account yet you can sign up here.

To start, create a new form with ++ Add New > New Form, call it Contact form and update the recipient email to the email where you wish to receive your form submissions. Then click Create Form.

Formspree new form modal

On the form details page, you will find the Form Endpoint:

Formspree form endpoint

Step 3: Set up the Formspree Form Endpoint

For both Reactive Forms and Template-driven Forms approaches, you’ll need to:

  1. Create a new form in Formspree (if you haven’t already)
  2. Get your form endpoint URL
  3. Replace {FORM_ID} in the component code with your actual form ID

For Reactive Forms, update the onSubmit() method:

this.http.post('https://formspree.io/f/your-form-id', this.form.value, {
  headers: { 'Accept': 'application/json' }
})

For Template-driven Forms, update the onSubmit() method:

this.http.post('https://formspree.io/f/your-form-id', {
  email: this.email,
  message: this.message
}, {
  headers: { 'Accept': 'application/json' }
})

Now your form is live! Fill it out and submit, and you should see a thank you message and receive an email if everything is set up correctly.

Bonus Section: Using Environment Variables for Different Environments

When working with multiple environments (development vs production), it’s a good idea to separate form submissions by using different form IDs.

Here’s how to manage that using Angular environment files. First of all, generate environments in your project by running the following command if you are not already using environments:

ng generate environments

Now, you will have three new files in your project:

your-app/src/environments
├── environment.development.ts
├── environment.staging.ts
└── environment.ts

In environments.development.ts, you can specify environment variables to use in development mode, like this;

export const environment = {
  production: false,
  formspreeFormId: 'yourDevFormId'
};

Similarly, you can specify environment values for staging environment in the environment.staging.ts file. The environment.ts file contains the default environment values, which will be used if you do not specify a particular environment when running your Angular app. You can learn more about environments here.

Now, you need to import the environment variables in your component by adding the following line:

import { environment } from './environments/environment';

Next, set up the URL to the component class:

formActionUrl = `https://formspree.io/f/${environment.formspreeFormId}`;

And finally, bind it in your component template:

<form
  [attr.action]="formActionUrl"
  class="fs-form"
  target="_top"
  method="POST"
>
  <div class="fs-field">
    <label class="fs-label" for="email">Email</label>
    <input class="fs-input" id="email" name="email" required />
  </div>
  <div class="fs-field">
    <label class="fs-label" for="message">Message</label>
    <textarea class="fs-textarea" id="message" name="message"></textarea>
  </div>
  <div class="fs-button-group">
    <button class="fs-button" type="submit">Submit</button>
  </div>
</form>

Now, when you build for staging (ng build --configuration staging), Angular will automatically swap in the staging form ID.

That’s it, you’re done!

You now have a fully functional contact form in your Angular app. You also know how to implement the form using both reactive forms and template driven forms.

To explore additional features like redirecting after submission or connecting to APIs, check out Formspree’s documentation.

You can find the complete code from this guide in this GitHub repo.


Got Feedback?