Processing Forms in AngularJS

by Juampy NRJanuary 22, 2014

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…</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…</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.