by Juampy NR on January 22, 2014 // Short URL

Processing Forms in AngularJS

AngularJS forms are nifty, HTML5 friendly, and incredibly fun to code!

AngularJS is an MVC JavaScript framework which elegantly separates controller, business and model logic in your application. Although it takes getting used to after years of writing server-side code, it simplifies a lot of backend logic in our projects and we've had wonderful success with it so far.

While working at the MSNBC project, we were asked to build a form that submits its results to a third party system via an HTTP request. Following up with the strategy outlined at our previous article about Decoupling the Drupal frontend with AngularJS, we implemented an AngularJS form that validated user input and submitted the data to an external service.

In this article, we'll walk through an example of this code and see how it works. The code is available in this GitHub repository, and you can view it in action. Let's take a look at the details.

Bootstrapping AngularJS

First, we must bootstrap AngularJS in the header of our HTML. We do that by adding a directive to the <html> tag which will define the name of our AngularJS module. We will also load three files in the <head> section:

  • The AngularJS library.
  • Promise Tracker: an AngularJS module to track a request's progress in order to display a loading alert.
  • Our AngularJS application, which will render and process the contact form.

Here is a snippet of our header:

<!DOCTYPE html>
<html lang="en" data-ng-app="myApp">
  <head>
    <title>AngularJS Form</title>
    <script type="text/javascript" src="http://code.angularjs.org/1.2.25/angular.min.js"></script>
    <script type="text/javascript" src="js/modules/promise-tracker.js"></script>
    <script type="text/javascript" src="js/app.js"></script>
    <link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css">
  </head>

That's it — we are ready to start using AngularJS in our page. Let's see what our form's HTML would look like.

Note: In this example we will be using data-ng-* instead of ng-* attributes when defining directives to keep the HTML W3C compliant.

Our contact form

Our form looks pretty much like any normal form, but with extra attributes that AngularJS will use. Let's start by having a quick glance at its markup, then we will dive into its details:

<div data-ng-controller="help">
  <div id="messages" class="alert alert-success" data-ng-show="messages" data-ng-bind="messages"></div>
  <div data-ng-show="progress.active()" style="color: red; font-size: 50px;">Sending&hellip;</div>
  <form name="helpForm" novalidate role="form">
    <div class="form-group">
      <label for="name">Your Name </label>
      <span class="label label-danger" data-ng-show="submitted && helpForm.name.$error.required">Required!</span>
      <input type="text" name="name" data-ng-model="name" class="form-control" required />
    </div>

    <div class="form-group">
      <label for="email">Your E-mail address</label>
      <span class="label label-danger" data-ng-show="submitted && helpForm.email.$error.required">Required!</span>
      <span class="label label-danger" data-ng-show="submitted && helpForm.$error.email">Invalid email!</span>
      <input type="email" name="email" data-ng-model="email" class="form-control" required /> 
    </div>

    <div class="form-group">
      <label for="subjectList">What is the nature of your request?</label>
      <span class="label label-danger" data-ng-show="submitted && helpForm.subjectList.$error.required">Required!</span>
      <select name="subjectList" data-ng-model="subjectList" data-ng-options="id as value for (id, value) in subjectListOptions" class="form-control" required>
        <option value=""></option>
      </select>
    </div>

    <div class="form-group">
      <label for="url">URL of Relevant Page</label>
      <span class="label label-danger" data-ng-show="submitted && helpForm.$error.url">Invalid URL format!</span>
      <input type="url" name="url" data-ng-model="url" class="form-control" />
    </div>

    <div class="form-group">
      <label for="comments">Description</label>
      <span class="label label-danger" data-ng-show="submitted && helpForm.comments.$error.required">Required!</span>
      <textarea name="comments" data-ng-model="comments" class="form-control" required></textarea>
    </div>

    <button data-ng-disabled="progress.active()" data-ng-click="submit(helpForm)" class="btn btn-default">Submit</button>
  </form>
</div>

As you can see, the above snippet has HTML on steroids that AngularJS will process. In order to manage the state of the form and its fields, we have defined a model for our form under the variable helpForm. We tell angular which model the form corresponds to by using the directive <form name="helpForm" novalidate>. Let's dive in deeper into this.

Our controller's scope

The controller is an AngularJS function that will take care of processing part of the HTML of the page. We define the scope of our controller using the ng-controller directive at <div data-ng-controller="help">. This means that inside our AngularJS application, we will implement a controller named help which will be in charge of processing the contents of this piece of HTML.

One of the main concepts that AngularJS introduces is Two-Way Data Binding between the Model and the View. You assign Model variables to the View (the HTML), then everytime values change in the controller, the View is automatically updated to reflect them and vice-versa.

Here is an example of Two-Way Data Binding:

<div id="messages" class="alert alert-success" data-ng-show="messages" data-ng-bind="messages"></div>

The above <div> tag will be our placeholder for status messages. The ng-bind directive binds it with a variable called messages that we will populate in our controller. So when we change a variable using the following code: $scope.messages = 'some text'; we will see it automatically in the screen. Isn't this just freaking great?

Form validation

Each form field in our form has different validation depending on its nature. Here is the HTML for the email field:

<label for="email">Your E-mail address</label>
<span class="label label-danger" data-ng-show="submitted && helpForm.email.$error.required">Required!</span>
<span class="label label-danger" data-ng-show="submitted && helpForm.$error.email">Invalid email!</span>
<input type="email" name="email" data-ng-model="email" class="form-control" required /> 

In the above snippet we define:

  • An error message to be displayed when the field has not been filled out on submission.
  • An error message to be displayed when the contents of the field are not a valid email address.
  • The field definition, attached to a model variable and defined as required.

Given the above statements, AngularJS takes care of keeping an object with the status of each form field. That is why we can automatically toggle an error message using the ng-show directive and evaluate the field state with helpForm.email.$error.required.

Form submission

Our form submit handler will take care of the following:

  1. If the data has not passed validation. Simply return. Errors will be shown automatically.
  2. If the data passed validation, prepare an object to be sent through a JSONP request.
  3. Once we get the response, evaluate it and inform the user depending on its data.

Note: JSONP is a communication technique to make HTTP requests to domains different than the current one. It is also an alternative to CORS.

Below is a snippet with the implementation of our form submit handler within the controller:

$scope.submit = function(form) {
  // Trigger validation flag.
  $scope.submitted = true;

  // If form is invalid, return and let AngularJS show validation errors.
  if (form.$invalid) {
    return;
  }

  // Default values for the request.
  var config = {
    params : {
      'callback' : 'JSON_CALLBACK',
      'name' : $scope.name,
      'email' : $scope.email,
      'subjectList' : $scope.subjectList,
      'url' : $scope.url,
      'comments' : $scope.comments
    },
  };

  // Perform JSONP request.
  var $promise = $http.jsonp('response.json', config)
    .success(function(data, status, headers, config) {
      if (data.status == 'OK') {
        $scope.name = null;
        $scope.email = null;
        $scope.subjectList = null;
        $scope.url = null;
        $scope.comments = null;
        $scope.messages = 'Your form has been sent!';
        $scope.submitted = false;
      } else {
        $scope.messages = 'Oops, we received your request, but there was an error processing it.';
        $log.error(data);
      }
    })
    .error(function(data, status, headers, config) {
      $scope.progress = data;
      $scope.messages = 'There was a network error. Try again later.';
      $log.error(data);
    })
    .finally(function() {
      // Hide status messages after three seconds.
      $timeout(function() {
        $scope.messages = null;
      }, 3000);
    });

  // Track the request and show its progress to the user.
  $scope.progress.addPromise($promise);
};

If you've ever implemented a JavaScript form submission, then the above should feel pretty familiar, but do you notice the AngularJS difference? We are displaying a response to the user by setting variables, instead of changing CSS classes or DOM elements. By altering variables in our scope, we are automatically altering the View. Angular handles updating the HTML that the user sees.

Extending our application

We saved some bits of our form until the end intentionally: an AngularJS module to track the progress of the form submission. Specifically, the angular-promise-tracker module was added in the <head> tag. In our view, we reference it in two places. First, we bind it to display a Sending… message like this:

<div data-ng-show="progress.active()" style="color: red; font-size: 50px;">Sending&hellip;</div>

We also use it to disable the submit button while the request is in progress:

<button data-ng-disabled="progress.active()" data-ng-click="submit(helpForm)" class="btn btn-default">Submit</button>

In our controller, we start by adding the module as a dependency of our custom module, then injecting it into our controller:

angular.module('myApp', ['ajoslin.promise-tracker'])
  .controller('help', function ($scope, $http, $log, promiseTracker) {

Next, we initiate the tracker by instantiating it and adding it to our controller's $scope:

// Inititate the promise tracker to track form submissions.
$scope.progress = promiseTracker();

Finally, at the end of our form submission handler we take the promise object returned by our JSONP request and add it to the tracker so it can check and inform about its progress to the user:

// Track the request and show its progress to the user.
$scope.progress.addPromise($promise);

The AngularJS Promise Tracker module will take care of updating the status of the progress object depending on the response data, and the View will be updated accordingly.

Go try it out!

Now, it is your turn to try this approach out. You can see the whole example and its source code at the Github repository.

Juampy NR

Senior Developer

Want Juampy NR to speak at your event? Contact us with the details and we’ll be in touch soon.

Comments

Add Your Comment

mradcliffe

Looks kind of ugly without

Looks kind of ugly without Javascript enabled when spidering the content for a search engine. ;-) Hope the page is private.

Reply

Alvaro Canepa

Using the new version of

Using the new version of promise-tracker is necessary add a line after $timeout

var promise = $timeout(function() { $scope.messages = null; }, 3000); $scope.progress.addPromise(promise);

Reply

RJ

What am I missing?

I get "Network error" every time. I added a response.json file with no change. I even tried a straight copy of the example and get the same "Network error." This is local on Firefox and on a test server in every browser.

Reply

RJ

Nevermind

I found the problem. Sorry, can't delete the post.

Reply

zach

I'm having the same issue

Hello, I'm having the same issue and I"m wondering what was your resolution to the issue? Any help is greatly appreciated.

Reply

RJ

Writing to the JSON file

So this example doesn't actually write to the JSON file? It never adds the submission. The response.json file is always:

angular.callbacks._0({ status : "OK" })

Reply

Juampy

Dummy server

No RJ, the example just focuses on the form itself.

The destination of the submission is a dummy JSON file that simply returns OK, as you mentioned. It would be up to you to replace that by a real server-side service that takes care of processing the submission.

Reply

zach

Keep getting a network error

Hello, I'm trying to use your code to be able to send an email from my site and I keep getting a Network error. I'm really hoping I can get this working very soon. Any help would be greatly appreciated.

Thanks, Zach

Reply

Juampy

JavaScript console

Zach, have you looked at the JavaScript console to see if there are further details of what is going on? I never experienced that error myself.

Reply

Paul

Post form

Are there examples of sending this form with Nodemailer or Sendgrid, in particualr I'm looing at passing the form data to either of those services in Node.js

Reply

Juampy

Out of scope

I have never used those services Paul so I do not know. In this example the form is submitted to a third party service that takes care of the actual email submission.

Reply

Mike

Nice job

This is the first I have seen someone using data-ng to keep compliance. It's hard to believe everyone else does it the other way.

Reply

Stephane

First time I see an AngularJS

First time I see an AngularJS attribute that does not start with ng-. Is this data-ng- prefix a new way ?

Also, it would be nice to see how to pre-populate the form fields with the help of a controller. Could a view controller be used to pre-populate the form and a submit controller to validate and save it ?

Kind Regards,

Stephane Eybert

Reply

Abul Hasan

data-ng and ng is same

data-ng and ng is same

So you can write data-ng-click or ng-click. AngularJS ignores the data- by default

You put data- just to satisfy html validation because it complains that ng-click is not a valid attribute..

Reply

Keren

Example for sending an email

By my understanding the php mailer will replace the response.json file, right? Can you please give an example on how to use this form to send an email?

Thank you!

Reply

Satya

How can I send the form data to an email

Pardon me for asking such basic question, but i just now started learning angular, I wanna know how can we send these form data to a specified email address.

Reply

Juampy

Needs server side code

Hi Sofia!

Unfortunately, it is not possible to send an email from JavaScript in the client side. You would send the form data to a server side application that receives the data and performs the email submission. Sending email is not a trivial task and you should contact a back end developer to give you a hand to set up the email submission process.

In case you want to do it yourself, PHPMailer (https://github.com/PHPMailer/PHPMailer) is a popular library to send emails.

Reply

Jayaraj

Submit on angular ready.

Let me first appreciate you for your beautiful blog. I need help to post data to an url, and open that webpage in an Iframe inside an angular modal. I am not able to submit the form in angular.element(document).ready function.

I am able to submit the form and web page opens in the Iframe withing the modal, if I use :

$(document).ready(function() { document.forms["myForm"].submit(); });

inside the modal's HTML page.

However, when I try to submit the form in modal's controller angular.element(document).ready(function() { document.forms["myForm"].submit(); }); it doesn't show up. The "document.forms["myForm"]" turn out to be null when I check in the console. How can I achieve this ?

Reply

Juampy

Use the browser console

Iframes are very tricky. I suggest you to type debugger; right before you do document.forms["myForm"].submit(); and then reload the page and do whatever you need to get to that piece of code. The browser will stop at the debugger; line so you can run JavaScript statements at the console to test how to reach to he form.

Note that since the form is in an iframe you should not use document, but instead do something like the following:

var iframe = document.getElementsByTagName('iframe')[0];

var doc = iframe.contentWindow.document;

doc.forms["myForm"].submit();

Reply

Add Your Comment