I've been usingNetlify for over a year now but just recently learned Angular. I was struggling to get forms working on my Netlify site so I decided to share my success. In this article I'm going to give you the steps needed to get your Angular app submitting forms. As an extra bonus, since I'm also learning RxJS, I'll show you how to implement this form using declarative or reactive programming.
This article will go overAngular template-driven forms. Using a reactive form would follow most of the same steps but I won't be discussing how to create a reactive form.
I'm going to assume some basic Angular knowledge and the know-how to create a component using ng generate. I'm also going to assume you are familiar with Netlify since I won't go through details such as setting up or deploying your site, although I will touch on it.
Netlify's form submissions are free for up to 100 submissions per month. To set it up on your account, it's pretty easy. As part of your deployment, Netlify will scan your HTML files looking for forms. Once it finds them it will register the form names with your site. So the first thing you have to do with your Angular app, is do something un-Angular-like. You have to code a form with form fields in your static HTML. I say static because Netlify isn't going to render your Angular pages to find an form. I put my form right inside my index.html file as a hidden element;
<form name="example-form" netlify hidden>
<input type="text" name="name"/>
<input type="text" name="email"/>
...
</form>
I only have 2 forms on my site so I can "get away" with putting in just in the index.html. I'm not sure how you would do it for many forms. My form includes all the fields I'll be using in both forms.
Let me point out the form's attributes. The "name" of the form is required and important, as this is the name of the form you have to use within your Angular code. Next is netlify which informs Netlify to add the form to your site. You could also use data-netlify if you need your form to be valid HTML. And don't forget to add hidden so the form isn't visible on your page.
Next, create an Angular component if you haven't done so already. In the same folder let's create a class that will hold the data for our form. I've called mine ExampleFormData with 3 required fields and 2 optional fields. The important field here is the form-name field. This is needed when submitting the form to match the name in your hidden form you created earlier.
export class ExampleFormData {
constructor(name: string, comment: string, formName: string = "example-form") {
this.name = name;
this.comment = comment;
this["form-name"] = formName
}
name: string;
email?: string;
url?: string;
comment: string;
"form-name": string;
}
Now we're going to set up our HTML code for our component. Right out of the gate we have a form field with the name of our form, a reference to the form that we'll use later in our Typescript.
<form #formRef="ngForm">
...
</form>
Next we add our fields, tying them to our component using [(ngModel)] and creating a template variable using the hash symbol. #name, #email, and so on for each form input. If we assign them ngModel we can use their states (valid, dirty, etc) in our template to control other behaviors. For simplicity, we'll only use them to enable our submit button but you could use it to determine when to show validation errors.
...
<label>Name: </label>
<input name="name" type="text" required [(ngModel)]="model.name" #name="ngModel" />
<label>Email: </label>
<input name="email" type="email" [(ngModel)]="model.email" #email="ngModel" email="model.email" />
...
<label>Comment/s: </label>
<textarea name="comment" required [(ngModel)]="model.comment" #comment="ngModel"></textarea>
The email directive is one that Angular provides for verifying if the email is valid.
I've also appended 2 div tags that bind to form submission. One is for displaying an error in case of an error. The other is our success message. I've put these outside of the form element so we can hide the form when it is submitted.
...
<div class="alert alert-danger" *ngIf="isError">There was an error with form submission: {{ error }}</div>
<div class="alert alert-success" *ngIf="isSuccess">Form submitted!</div>
When using Angular's generator, you should have a Typescript file with the similar code:
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
@Component({
selector: 'app-example-form',
templateUrl: './example-form.component.html',
styleUrls: ['./example-form.component.scss']
})
export class ExampleFormComponent {
}
To tie in our form we add a property named "form" with the ViewChild annotation. The annotation takes in the name of the form reference in our template. In our case, this is "formRef". The code should look like this:
@ViewChild('formRef')
form!: NgForm;
This will let listen to the form'sngSubmitEventEmitter. It will emit an event when the form is submitted. Before we get to that let's set up the rest of our class members. We'll add a member to hide the submit button when the form is submitting plus the variables to tell our template if the submission was successful or not.
beingSubmitted: boolean = false;
isSuccess = false;
isError = false;
error?: string;
Next we create our model variable and assign it to a new instance of it. Instead of referencing the name of the Netlify form in our template, I'm doing it here since the value won't change. (Although my class also defaults to it.) Don't forget the import statement to your class.
import { ExampleFormData } from './example-form-data';
...
model: ExampleFormData = { name: '', comment: '', 'form-name': 'post-comment-form', email: '', url: '' };
To finish implementing the component we're going to need a service that we can call. Let's jump over to this service I am calling NetlifyFormsService.
We're going to write a service that we can call from our form component. This service will handle the actual HTTP call. Create a new file called netlify-forms.service.ts. We'll need to inject the HttpClient as well as make our service itself injectable. We're also going to use our data class and a few other things.
import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';
import { Injectable, isDevMode } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ExampleFormData } from './example-form-data';
@Injectable({
providedIn: 'root'
})
export class NetlifyFormsService {
constructor(private http: HttpClient) { }
}
The meat of the service we'll put into a method called submitFeedback. In here we'll use the spread operator to put our form class into a new HttpParams class needed by the HttpClient. Then we call the post method on HttpClient. For testing purposes I'm using isDevMode(), an Angular method to determine whether we are deployed in production or not. For testing I'm standing up a separate server listening on port 8000 that just responds with a simple 200. Otherwise, you'll want to use '/' as the URL to post your data. Another important piece is you need to send your form URL encoded so add the "Content-Type" header to your request. Lastly we'll catch any errors by using thecatchError RxJS operator.
submitFeedback(commentEntry: ExampleFormData): Observable<any> {
const entry = new HttpParams({
fromObject: {
...commentEntry
}
});
return this.http
.post(
isDevMode() ? 'http://localhost:8000/' : '/',
entry.toString(),
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'text'
}
)
.pipe(catchError(this.handleError));
}
To finalize our service we implement the handleError method which will throw an error with our custom error message.
private handleError(err: HttpErrorResponse) {
let errMsg = '';
if (err.error instanceof ErrorEvent) {
errMsg = `A client-side error occurred: ${err.error.message}`;
} else {
errMsg = `A server-side error occurred. Code: ${err.status}. Message: ${err.message}`;
}
return throwError(() => new Error(errMsg));
}
Back to the component and in the home stretch! To finish my component, I decided to practice my reactive programming. I know I want to listen for the event fired when the submit button is pressed and eventually listen to the event from our service. I start with the ngSubmit EventEmitter and pipe this into some RxJS operators. The tap operator is used as a way to call some code that won't affect the observable. I'm setting the beingSubmitted variable to true so I can hide the submit button. Then I am use theswitchMap operator to start listening to the service's Observable. I then subscribe to this new observable using the subscribe method with an object containing a method to execute when the next event is sent. It will also be the last as the HTTP client returns a "finite" Observable. (Read more on the subject here.) But because subscribe is used in other cases also, the method is named next. The error method, you guessed it, handles if there was an error.
ngAfterViewInit() {
this.form.ngSubmit
.pipe(
tap(() => this.beingSubmitted = true),
switchMap(() => this.netlifyForms.submitFeedback(this.model as ExampleFormData)),
)
.subscribe({
next: () => {
this.beingSubmitted = false;
this.isSuccess = true;
},
error: (err) => {
this.isError = true;
this.error = err;
}
});
}
You should use your regular way to deploy to Netlify. For me, that's pushing to my Github repo. In the output of the Netlify deploy you should see something relating to your form.
8:11:10 PM: Detected form fields: - name - email - url - comment
It's been a journey for me to Angularize my site but I've learned a lot. I hope you learned a little bit. Source code follows:
<form name="example-form" netlify hidden>
<input type="text" name="name"/>
<input type="text" name="email"/>
<input type="text" name="url"/>
<input type="text" name="comment"/>
</form>
<form class="needs-validation" name="example-form" novalidate #formRef="ngForm" [hidden]="isSuccess || isError">
<label for="name">Name: </label>
<input name="name" type="text" id="name" required="required" [(ngModel)]="model.name" #name="ngModel" />
<label for="email">Email: </label>
<input name="email" type="email" id="email" [(ngModel)]="model.email" #email="ngModel" email="model.email" />
<label for="url">URL: </label>
<input name="url" type="text" id="url" [(ngModel)]="model.url" #url="ngModel" />
<label for="comment">Comment/s: </label>
<textarea name="comment" id="comment" required="required" [(ngModel)]="model.comment" #comment="ngModel" class="form-control"></textarea>
<button type="submit" *ngIf="!beingSubmitted" id="submitButton" [disabled]="!name.valid || !comment.valid || (email.touched && email.value != '' && !email.valid)">Submit</button>
</form>
<div class="alert alert-danger" *ngIf="isError">There was an error with form submission: {{ error }}</div>
<div class="alert alert-success" *ngIf="isSuccess">Form submitted!</div>
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { switchMap, tap } from 'rxjs';
import { NetlifyFormsService } from '../../../../../../netlify-forms.service';
import { ExampleFormData } from './example-form-data';
@Component({
selector: 'app-example-form',
templateUrl: './example-form.component.html',
styleUrls: ['./example-form.component.scss']
})
export class ExampleFormComponent implements AfterViewInit {
@ViewChild('formRef')
form!: NgForm;
beingSubmitted: boolean = false;
isSuccess = false;
isError = false;
error?: string;
model: ExampleFormData = { name: '', comment: '', 'form-name': 'comment-form', email: '', url: '' };
constructor(private netlifyForms: NetlifyFormsService) {
}
ngAfterViewInit() {
this.form.ngSubmit
.pipe(
tap(() => this.beingSubmitted = true),
switchMap(() => this.netlifyForms.submitFeedback(this.model as ExampleFormData)),
)
.subscribe({
next: () => {
this.beingSubmitted = false;
this.isSuccess = true;
},
error: (err) => {
this.isError = true;
this.error = err;
}
});
}
}
import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';
import { Injectable, isDevMode } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ExampleFormData } from './example-form-data';
@Injectable({
providedIn: 'root'
})
export class NetlifyFormsService {
constructor(private http: HttpClient) { }
submitFeedback(commentEntry: ExampleFormData): Observable<any> {
const entry = new HttpParams({
fromObject: {
...commentEntry
}
});
return this.http
.post(
isDevMode() ? 'http://localhost:8000/' : '/',
entry.toString(),
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'text'
}
)
.pipe(catchError(this.handleError));
}
private handleError(err: HttpErrorResponse) {
let errMsg = '';
if (err.error instanceof ErrorEvent) {
errMsg = `A client-side error occurred: ${err.error.message}`;
} else {
errMsg = `A server-side error occurred. Code: ${err.status}. Message: ${err.message}`;
}
return throwError(() => new Error(errMsg));
}
}
To test my form I use a simple NodeJS program to respond to my form request. To run it: node filename.js
const http = require("http");
const host = 'localhost';
const port = 8000;
const requestListener = function (req, res) {
console.log("received request", req);
res.setHeader("Access-Control-Allow-Origin", "*")
res.writeHead(200);
res.end("success");
@;
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host@:${port@`);
@);