Thursday, June 25, 2015

Tips for building AngularJS forms with validation and Bootstrap

Lets get started, in this article we will be using AngularJS 1.3.15, because that's what version the project I'm currently working on uses. Keep in mind, this stuff changes all the time but, for now, this seems to be the "correct" way.

You've probably been using AngularJS for a while now, you also know how forms work, and you probably have forms in you're AngularJS project that are collecting data and everything is working fine. So why do you still need help with forms? That's where I was at a few days ago. I've been building forms with a basic understanding of ng-model and FormController, and they work, but I've been writing a lot of extra JavaScript to do it.

A developer on my team, figured out a better way to do a lot of the validation using AngularJS and it's was so simple. It was right here the whole time: code.angularjs.org/1.3.16/docs/guide/forms.

So, let's put a contact us form together using AngularJS and Bootstrap.
angular.module("mainApp")
    .controller("ContactCtrl", function ($scope, $log) {
        "use strict";
});
<!-- form.tpl.html -->
<form class="form-horizontal" name="contactForm" novalidate>
</form>

Starting with a basic controller and a form template. We already have a ton of stuff setup here thanks to AngularJS. AngularJS adds ng-form code to all <form> and <ng-form> tags that sets up a property on the $scope that matches the name you gave the form. In this example, even though we didn't create it, we can now access $scope.contactForm which is an instance of FormController.

For the HTML template, I'm going to copy the code from the link (code.angularjs.org/1.3.16/docs/guide/forms) to setup a name and email field in our form. Before we use it though, let's update the code to match what Bootstrap has for the basic HTML structure for a horizontal form using their CSS classes.
<!-- form.tpl.html -->
<form class="form-horizontal" name="contactForm">

    <div class="form-group">
        <label for="inputName" class="col-sm-2 control-label">Name:</label>
        <div class="col-sm-10">
            
            <input type="text" id="inputName" name="uName"
                   class="form-control"
                   placeholder="Enter your name"
                   ng-model="user.name"
                   required />
            
            <div ng-show="contactForm.$submitted || contactForm.uName.$touched">
                <span ng-show="contactForm.uName.$error.required">
                    Tell us your name.</span>
            </div>
            
        </div>
    </div>

    <div class="form-group">
        <label for="inputEmail" class="col-sm-2 control-label">E-mail:</label>
        <div class="col-sm-10">
            
            <input type="email" id="inputEmail" name="uEmail"
                   class="form-control"
                   placeholder="E-mail"
                   ng-model="user.email"
                   required />
            
            <div ng-show="contactForm.$submitted || contactForm.uEmail.$touched">
                <span ng-show="contactForm.uEmail.$error.required">
                    Tell us your email.</span>
                <span ng-show="contactForm.uEmail.$error.email">
                    This is not a valid email.</span>
            </div>
            
        </div>
    </div>

    <div class="form-group">
        <div class="col-sm-offset-2 col-sm-10">
            
            <button type="button" 
                    class="btn btn-default" 
                    ng-click="reset()">Reset</button>
            
            <button type="submit" 
                    class="btn btn-primary" 
                    ng-click="save()" 
                    ng-disabled="contactForm.$invalid">Save</button>
        </div>
    </div>

</form>

In the code above, everything looks pretty standard.

The name attribute
The thing to look at is that on each input we have added a name attribute. This is what I was missing when I first tried to setup forms using validation in this way. Adding the name "uName" will in turn add that property to the parent formController instance "contactForm".

Now, that element's ng-model scope can be accessed via $scope.contactForm.uName. This is big because then we can easily add validation messages like ng-show="contactForm.uEmail.$error.required" in a list under each form input.

$touched
Another thing I overlooked was the wrapping div around the validation messages. With out it, as soon as the form loads, we see the required error message and the input it outlined in red. I thought this was an issue with AngularJS and spent a lot of time getting around it, but actually I just need to use the $touched property. This will cause the input not to be shown as invalid until the user causes a blur event on the field.

$submitted
The last thing I want to point out is the contactForm.$submitted property. Since AngularJS converts all form tags to ng-form, they no longer submit to the server by default when you mark a button as type="submit". What happens is that this property on formController is set to true, which allows us another chance to show any validation errors in the form.

Let's do a little work on our controller now.
angular.module("mainApp")
    .controller("ContactCtrl", function ($scope, $log, userService) {
        "use strict";

    //we need to create a new user to hold our form data. 
    $scope.user = userService.createUser();

    //we'll assume that userService.createUser() returns an object like this:
    //{ name : "", email : "" };

    $scope.reset = function () {
        //here we can just do a simple call to create a new user again
        $scope.user = userService.createUser();
    };

    $scope.save = function () {
        //we can use our formController to do a simple check to see if the form is valid
        if( $scope.contactForm.$valid ) {
            //all fields are valid, so submit to server.
            userService.save( $scope.user );
        }
    };
});

As you can see, we can let AngularJS do most of the work with forms and validation. No more if/else statements like this to check all the custom validation on your form elements.
    $scope.save = function () {
        if($scope.user.name.length === 0) {
             $scope.userNameError = "Name is Required!";
        } else if($scope.user.email.length > 0) {
             $scope.userEmailError = "Email is Required!";
        } else if(! isEmail($scope.user.email) ) {
             $scope.userEmailError = "This is not a valid email.";
        } else {
             //form is valid
             userService.save( $scope.user );
        }
    };

But you never wrote code like that anyway, right?