Validation Gotchas to Watch For When Using Angular

Thursday, July 11, 2013

Introduction

Validating a form in Angular is rather straight forward and it is pretty unobtrusive when it comes to the amount of code you have to right, but I thought I would quickly share some gotchas that I experienced when validating a form.

<form name="taskForm" ng-submit="save()" class='form-horizontal' novalidate>

The Form Tag and Controller

First thing to note is that if you want your submit function to fire so you can validate the form in your controller, you need to add the novalidate attribute to your form tag. Otherwise, the browser's built in validating handler will kick and prevent the form submission from firing.

if ($scope.taskForm.$valid === falsereturn;

Notice the form name is automatically attached to scope so it can be validated.  Angular has some built evaluation function you can use to validate the form.

To keep the errors from showing until the user has submitted the form: I created an isSubmitted variable on the $scope object and setting it true when the user clicks the submit button.

$scope.isSubmitted = true;

The Input tag.

Another thing to note is that if you want to use the other custom attributes like email, min, max, etc, then you need to make sure your use the correct input type in your tag. If you don’t, those validation errors will not fire.

Here is a number example:

<div class="control-group" ng-class="{error: isSubmitted && (timesheetForm.rate.$error.required || timesheetForm.rate.$error.number || timesheetForm.rate.$error.min || timesheetForm.rate.$error.max)}">
                <label class="control-label" for="timesheet-rate">Actual Rate:</label>
                <div class="controls">
                    <input type="number" ng-model="timesheet.rate" class="input-mini" 
id="timesheet-rate" name="rate" required min="1" max="999"/>
                    <span class="help-inline" 
ng-show="isSubmitted && timesheetForm.rate.$error.required">Actual rate is required.</span>
                    <span class="help-inline" 
ng-show="isSubmitted && timesheetForm.rate.$error.number">Actual rate is not a valid number.</span>
                    <span class="help-inline" 
ng-show="isSubmitted && timesheetForm.rate.$error.min">Actual rate must be greater than 0.</span>
                    <span class="help-inline" 
ng-show="isSubmitted && timesheetForm.rate.$error.max">Actual rate must be less than 999.</span>
                </div>
            </div>

Here is an email example:

            <div class="control-group" ng-class="{error: isSubmitted && (clientForm.email.$error.required || clientForm.email.$error.email)}">
                <label class="control-label" for="email-address">Email Address:</label>
                <div class="controls">
                    <input type="email" ng-model="client.contactEmail" class="input-xlarge" id="email-address" name="email" required/>
                    <span class="help-inline" ng-show="isSubmitted  && clientForm.email.$error.required">Email is required.</span>
                    <span class="help-inline" ng-show="isSubmitted  && clientForm.email.$error.email">Not a valid email address.</span>
                </div>
            </div>

You Need a Name:

You need to include the name attribute in your input tag and the name of the tag cannot have a "-" in the name. Same thing here; the validation will work with the dash.

Styling Using ng-class

For styling errors in Twitter Bootstrap you have to provide an error class in the control-group div tag which is the parent of the input tag that is being validated. To do that with an input element that several validations you can evaluate several conditions at once.

Here I am using the ng-class directive to validate several different scenarios.

<div class="control-group" ng-class="{error: isSubmitted && (timesheetForm.hours.$error.required || timesheetForm.hours.$error.number || timesheetForm.hours.$error.min || timesheetForm.hours.$error.max)}">

Here is the entire form.

        <form name="timesheetForm" ng-submit="save()" class='form-horizontal' novalidate>
            
            <div class="control-group" ng-class="{error: isSubmitted && timesheetForm.timesheetName.$error.required}">
                <label class="control-label" for="timesheet-name">Timesheet Name:</label>
                <div class="controls">
                    <input type="text" ng-model="timesheet.timesheetName" 
class="input-xlarge" id="timesheet-name" name="timesheetName" ng-focus required/>
                    <span class="help-inline" 
ng-show="isSubmitted && timesheetForm.timesheetName.$error.required">Timesheet name is required.</span>
                </div>
            </div>
            <div class="control-group">
                <label class="control-label" for="timesheet-description">Description:</label>
                <div class="controls">
                    <textarea ng-model="timesheet.timesheetDescription" 
class="input-xlarge" id="timesheet-description"  cols="20" rows="10"></textarea>
                </div>
            </div>
            <div class="control-group" ng-class="{error: isSubmitted && timesheetForm.startDate.$error.required}">
                <label class="control-label" for="timesheet-start-date">Start Date:</label>
                <div class="controls">
                    <input type="text" jq-datepicker ng-model="$parent.timesheet.startDate" 
class="input-small" id="timesheet-start-date" name="startDate" required/>
                    <span class="help-inline" 
ng-show="isSubmitted && timesheetForm.startDate.$error.required">Start date is required.</span>
                </div>
            </div>
            <div class="control-group" ng-class="{error: isSubmitted && (timesheetForm.hours.$error.required || timesheetForm.hours.$error.number || timesheetForm.hours.$error.min || timesheetForm.hours.$error.max)}">
                <label class="control-label" for="timesheet-hours">Actual Hours:</label>
                <div class="controls">
                    <input type="number" ng-model="timesheet.actualHours" class="input-mini" id="timesheet-hours" name="hours" required min="1" max="999"/>
                    <span class="help-inline" ng-show="isSubmitted && timesheetForm.hours.$error.required">Actual hours are required.</span>
                    <span class="help-inline" ng-show="isSubmitted && timesheetForm.hours.$error.number">Actual hours is not a valid number.</span>
                    <span class="help-inline" ng-show="isSubmitted && timesheetForm.hours.$error.min">Actual hours must be greater than 0.</span>
                    <span class="help-inline" ng-show="isSubmitted && timesheetForm.hours.$error.max">Actual hours must be less than 999.</span>
                </div>
            </div>
            <div class="control-group" ng-class="{error: isSubmitted && (timesheetForm.rate.$error.required || timesheetForm.rate.$error.number || timesheetForm.rate.$error.min || timesheetForm.rate.$error.max)}">
                <label class="control-label" for="timesheet-rate">Actual Rate:</label>
                <div class="controls">
                    <input type="number" ng-model="timesheet.rate" class="input-mini" id="timesheet-rate" name="rate" required min="1" max="999"/>
                    <span class="help-inline" ng-show="isSubmitted && timesheetForm.rate.$error.required">Actual rate is required.</span>
                    <span class="help-inline" ng-show="isSubmitted && timesheetForm.rate.$error.number">Actual rate is not a valid number.</span>
                    <span class="help-inline" ng-show="isSubmitted && timesheetForm.rate.$error.min">Actual rate must be greater than 0.</span>
                    <span class="help-inline" ng-show="isSubmitted && timesheetForm.rate.$error.max">Actual rate must be less than 999.</span>
                </div>
            </div>
            <div class="control-group">
                <label class="control-label" for="timesheet-revision-number">Revision Number:</label>
                <div class="controls">
                    <input type="text" ng-model="timesheet.revisionNumber" class="input-small" id="timesheet-revision-number"/>
                </div>
            </div>
            <div class="control-group" ng-class="{error: isSubmitted && timesheetForm.status.$error.required}">
                <label class="control-label" for="timesheet-status">Status:</label>
                <select ng-model="timesheet.statusId" 
data-ng-options="t.statusId as t.statusName for t in statuses" id="timesheet-status" name="status" required></select>
                <span class="help-inline" ng-show="isSubmitted && timesheetForm.stats.$error.required">Status selection is required.</span>
            </div>
            <div class="control-group" ng-class="{error: isSubmitted && timesheetForm.user.$error.required}">
                <label class="control-label" for="timesheet-user">User:</label>
                <select ng-model="timesheet.userId" data-ng-options="m.userId as m.userName for m in members" id="timesheet-user" name="user" required></select>
                <span class="help-inline" ng-show="isSubmitted && timesheetForm.user.$error.required">Member selection is required.</span>
            </div>
            <div class="form-actions">
                <button class="btn btn-primary" >Save</button>
                <button class="btn" ng-click="cancel()" >Remove</button>
            </div>
        </form>

Hope that helps.

comments powered by Disqus