Working with Forms with AngularJS: Providing Form Validation Feedback

The overall effect of the example in the previous section was simple: The OK button is disabled until all of the input elements are valid, preventing the user from providing badly formatted or incomplete data. Behind the scenes, AngularJS performs validation checks as the user is interacting with the form elements, and we can use the information that these checks provide to give the user meaningful feedback in real time, rather than waiting until they are ready to submit the data. In the sections that follow, I demonstrate the two mechanisms that AngularJS provides for reporting real-time validation: classes and variables.

1. Using CSS to Provide Feedback

Each time the user interacts with an element that is being validated, AngularJS checks its state to see whether it is valid. The validity checks depend on the element type and how it has been configured. For a check box, for example, the check is generally as simple as checking to see that the user has checked the box. An example for an input element whose type is email might be the user has provided a value, that the value is formatted as a valid e-mail address, and that the e-mail address is within a specific domain.

AngularJS reports on the outcome of these validation checks by adding and removing the elements it validates from a set of classes, which can be combined with CSS to provide feedback to the user by styling the element. AngularJS uses four basic classes, which I have listed in Table 12-4.

AngularJS adds and removes elements that are being validated from these classes after every interaction, which means you can use these classes to give keystroke-by-keystroke, click-by-click feedback to the user, both for the overall form and for individual elements. Listing 12-6 shows the use of these classes.

Listing 12-6. Using the Validation Classes to Provide Feedback in the forms.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Forms</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.addUser = function (userDetails) {

$scope.message = userDetails.name

+ ” (” + userDetails.email + “) (” + userDetails.agreed + ”)”;

}

$scope.message = “Ready”;

});

</script>

<style>

form .ng-invalid.ng-dirty { background-color: lightpink; }

form .ng-valid.ng-dirty { background-color: lightgreen; }

span.summary.ng-invalid { color: red; font-weight: bold; }

span.summary.ng-valid { color: green; }

</style>

</head>

<body>

<div id=”todoPanel” class=”panel” ng-controller=”defaultCtrl”>

<form name=”myForm” novalidate ng-submit=”addUser(newUser)”>

<div class=”well”>

<div class=”form-group”>

<label>Name:</label>

<input name=”userName” type=”text” class=”form-control”

required ng-model=”newUser.name”>

</div>

<div class=”form-group”>

<label>Email:</label>

<input name=”userEmail” type=”email” class=”form-control”

required ng-model=”newUser.email”>

</div>

<div class=”checkbox”>

<label>

<input name=”agreed” type=”checkbox”

ng-model=”newUser.agreed” required>

I agree to the terms and conditions

</label>

</div>

<button type=”submit” class=”btn btn-primary btn-block”

ng-disabled=”myForm.$invalid”>OK</button>

</div>

<div class=”well”>

Message: {{message}}

<div>

Valid:

<span class=”summary”

ng-class=”myForm.$valid ? ‘ng-valid’ : ‘ng-invalid'”>

{{myForm.$valid}}

</span>

</div>

</div>

</form>

</div>

</body>

</html> 

I have defined four CSS styles that select elements that belong to the classes I described in Table 12-4. The first two styles select elements that are members of the ng-dirty class, which is applied to elements only after the user has interacted with them. (Before the interaction, elements are members of the ng-pristine class.) Elements that have valid content are members of the ng-valid class and are shaded a light green color. Elements that have invalid content are members of the ng-invalid class and are shaded in light pink (which is as close to a light red shade as I can easily get). Combining the ng-valid and ng-valid classes with ng-dirty in the CSS selector means that the real-time feedback about element validity doesn’t begin until the user starts interacting with the element. The best way of seeing how this works is to load the forms.html file into the browser and start entering an e-mail address into the input element whose type is email. Before you start typing, the input element will be in the ng-pristine class, and none of the CSS styles I have defined will be applied, as shown in Figure 12-5.

As you start to type, AngularJS moves the element from the ng-pristine class, adds it to the ng-dirty class, and starts validating the content. In Figure 12-6, you can see the effect of the first few characters of an e-mail address. The element was added to the ng-invalid class after the first character was entered because the content isn’t in the right format for an e-mail address.

Finally, as I complete the e-mail address, AngularJS removes the element from the ng-invalid class and adds it to ng-valid, reflecting the fact that I have provided a properly formatted e-mail address, as shown in Figure 12-7.

You can, of course, use the special variables that AngularJS provides to add and remove elements from these classes yourself. This is what I have done with the span element that I added in Listing 12-6:

<div>

Valid: <span class=”summary” ng-class=”myForm.$valid ? ‘ng-valid1 : ‘ng-invalid'”>

{{myForm.$valid}}

</span>

</div>

I used the ng-class directive, which I described in Chapter 11, to add and remove the span element from the ng-valid and ng-invalid classes based on the validation status of the complete form. The styles that I defined for the span element set the color to red when the form is invalid and green otherwise. (The text itself is set using a data binding to the $valid variable that AngularJS associates with the form element, which I introduced in the basic validation section and describe in detail later in this chapter.)

1.1. Providing Feedback for Specific Validation Constraints

The classes that I listed in Table 12-4 give an overall indication of the validation state of an element, but AngularJS also adds elements to classes to give specific information about each of the validation constraints that apply to an element. The name of the class that is used is based on the corresponding attribute, as demonstrated in Listing 12-7.

Listing 12-7. Providing Feedback About Specific Constraints in the forms.html File

<style>

form .ng-invalid-required.ng-dirty { background-color: lightpink; }

form .ng-invalid-email.ng-dirty { background-color: lightgoldenrodyellow; }

form .ng-valid.ng-dirty { background-color: lightgreen; }

span.summary.ng-invalid {color: red; font-weight: bold; }

span.summary.ng-valid { color: green }

</style>

In the highlighted styles, I have changed the selectors so that they are applied to specific validation problems.

I applied two validation constraints to one of the input elements in the example: using the required attribute to mandate a value and setting the type attribute to email, requiring that value to be formatted as an e-mail address.

AngularJS will add the element to the ng-valid-required and ng-invalid-required classes to signal compliance with the required attribute and use the ng-valid-email and ng-invalid-email classes to single compliance with the formatting constraint.

You need to be careful when working with these classes because it is possible for an element to be valid for one constraint but not another. For example, an element whose type attribute is email will be valid when the input is empty, meaning that the element will be in the ng-valid-email class and the ng-invalid-required class at the same time. This is an artifact of the HTML specification, and thorough testing is required to make sure you don’t give the user nonsensical feedback (although this can be addressed by showing text cues, which I describe in the next section).

2. Using the Special Variables to Provide Feedback

As I described in Table 12-3 earlier in the chapter, AngularJS provides a set of special variables for form validation that you can use in views to check the validation status of individual elements and of the form as a whole. I used these variables to control the disabled state of a button in earlier examples by applying the ng-disabled directive, but they can also be used to control visibility of elements that give feedback to the user by applying them with the ng-show directive, as illustrated in Listing 12-8. For this example, I removed some of the elements from previous examples so I can keep the listing simple.

Listing 12-8. Using Validation Variables to Control Element Visibility in the forms.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Forms</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.addUser = function (userDetails) {

$scope.message = userDetails.name

+ ” (” + userDetails.email + “) (” + userDetails.agreed + “)”;

}

$scope.message = “Ready”;

});

</script>

<style>

form .ng-invalid-required.ng-dirty { background-color: lightpink; }

form .ng-invalid-email.ng-dirty { background-color: lightgoldenrodyellow; }

form .ng-valid.ng-dirty { background-color: lightgreen; }

span.summary.ng-invalid { color: red; font-weight: bold; }

span.summary.ng-valid { color: green; }

div.error {color: red; font-weight: bold;}

</style>

</head>

<body>

<div id=”todoPanel” class=”panel” ng-controller=”defaultCtrl”>

form name=”myForm” novalidate ng-submit=”addUser(newUser)”>

<div class=”well”>

<div class=”form-group”>

<label>Email:</label>

<input name=”userEmail” type=”email” class=”form-control”

required ng-model=”newUser.email”>

<div class=”error”

ng-show=”myForm.userEmail.$invalid && myForm.userEmail.$dirty”>

<span ng-show=”myForm.userEmail.$error.email”>

Please enter a valid email address

</span>

<span ng-show=”myForm.userEmail.$error.required”>

Please enter a value

</span>

</div>

</div>

<button type=”submit” class=”btn btn-primary btn-block”

ng-disabled=”myForm.$invalid”>OK</button>

</div>

</form>

</div>

</body>

</html>

I have added a new div element to display validation messages to the user. The visibility of the div element is controlled by the ng-show directive, which will display the element if the input element has not passed its validation checks and if it is dirty.

Tip The continuous nature of AngularJS validation means that an empty, pristine input element with the required attribute is invalid, because it contains no value. I don’t want to display an error message before the user has started to even enter data, so I check that $dirty is true, indicating that the user has interacted with the element, before displaying the error message.

Notice how I refer to the input element in order to access the special validation variables:

<div class=”error” ng-show=“myForm.userEmail.$invalid && myForm.userEmail.$dirty”>

I refer to the element via the name value of the form element combined with the name of the input element, separated by a period: myForm.userEmail. It is for this reason that I have emphasized the importance of applying the name attribute to elements that are being validated.

Within the div element, I have defined an error message contained in a span element for both of the validation constraints I applied to the input element. I control the visibility of these elements by using the special $error variable, which returns an object with properties representing the validation constraints. I can tell whether the required constraint has not been met by checking if $error.required is true and checking the formatting of the value the user has entered by seeing whether $error.email is true. The $error object will contain properties for all the constraints applied to an element; you can see the effect I have created in Figure 12-8. (To create the first panel in the figure, I typed a character and then deleted it to switch the element from pristine to dirty.)

2.1. Reducing the Number of Feedback Elements

The previous example neatly demonstrates the way that the special validation variables can be combined with other directives in order to improve the user experience, but you can end up with a lot of elements in your markup that duplicate the same messages. As Listing 12-9 shows, it is a simple matter to consolidate these messages into a controller behavior.

Listing 12-9. Consolidating Validation Feedback in the forms.html File

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.addUser = function (userDetails) {

$scope.message = userDetails.name

+ ” (” + userDetails.email + “) (” + userDetails.agreed + “)”;

}

$scope.message = “Ready”;

$scope.getError = function (error) {

if (angular.isDefined(error)) {

if (error.required) {

return “Please enter a value”;

} else if (error.email) {

return “Please enter a valid email address”;

}

}

}

});

</script>

The behavior I defined is called getError, and it receives the $error object from validation elements and returns a string based on the properties that are defined. The $error object isn’t defined until there is a problem, so I use the angular.isDefined method (which I described in Chapter 5) so that I don’t try to read properties from a nonexistent object. (For simplicity, I return an error message for the first property I find, but the $error object will have multiple properties if there are multiple validation errors.) I can simplify my markup by using the behavior in a data binding, as shown in Listing 12-10.

Listing 12-10. Consuming the Behavior in the forms.html File

<div class=”form-group”>

<label>Email:</label>

<input name=”userEmail” type=”email” class=”form-control”

required ng-model=”newUser.email”>

<div class=”error” ng-show=”myForm.userEmail.$invalid && myForm.userEmail.$dirty”>

{{getError(myForm.userEmail.$error)}}

</div>

</div>

The effect is the same, but my error messages are defined in a single place, which makes them easier to change and easier to test. I could further simplify the validation process by creating a custom directive, which is a topic I cover in Chapters 15-17.

3. Deferring Validation Feedback

AngularJS form validation is supremely responsive, updating the validation status of every element after every interaction. This can be a little too responsive, and it is easy to display error messages to users in a way that feels aggressive, especially compared to more traditional form validation, which may defer displaying errors until the user tries to submit the form.

If you don’t like the default approach that AngularJS takes, you can easily build on the basic features to defer the feedback. In Listing 12-11, I show you how to defer feedback until the button is clicked.

Listing 12-11. Deferring the Display of Validation Error Messages in the forms.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Forms</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.addUser = function (userDetails) {

if (myForm.$valid) {

$scope.message = userDetails.name

+ ” (” + userDetails.email + “) (“

+ userDetails.agreed + “)”;

} else {

$scope.showValidation = true;

}

}

$scope.message = “Ready”;

$scope.getError = function (error) {

if (angular.isDefined(error)) {

if (error.required) {

return “Please enter a value”;

} else if (error.email) {

return “Please enter a valid email address”;

}

}

}

});

</script>

<style>

form.validate .ng-invalid-required.ng-dirty { background-color: lightpink; }

form.validate .ng-invalid-email.ng-dirty {

background-color: lightgoldenrodyellow; }

div.error { color: red; font-weight: bold; }

</style>

</head>

<body>

<div id=”todoPanel” class=”panel” ng-controller=”defaultCtrl”>

<form name=”myForm” novalidate ng-submit=”addUser(newUser)”

ng-class=”showValidation ? ‘validate’ : ””>

<div class=”well”>

<div class=”form-group”>

<label>Email:</label>

<input name=”userEmail” type=”email” class=”form-control”

required ng-model=”newUser.email”>

<div class=”error” ng-show=”showValidation”>

{{getError(myForm.userEmail.$error)}}

</div>

</div>

<button type=”submit” class=”btn btn-primary btn-block”>OK</button>

</div>

</form>

</div>

</body>

</html>

With an example like this, you can really see how the generic functionality offered by each directive can be combined to create custom interactions in a web app. I have modified my addUser behavior so that it checks the validity of the whole form and sets an implicitly defined model property to true if validation feedback should be displayed. The addUser behavior is not called until the form is submitted, which means that the user can enter anything they like into the input element without getting feedback.

If the form is submitted and has validation errors, setting the model property to true reveals the validation feedback, which I control through a class that I applied to the form element and that I target with my CSS selectors.

I use the same model property on the div element that contains the text feedback, although just to simplify the view logic. The result is that validation feedback isn’t displayed to the user until the form is first submitted, as illustrated in Figure 12-9, after which the normal real-time feedback is provided.

Source: Freeman Adam (2014), Pro AngularJS (Expert’s Voice in Web Development), Apress; 1st ed. edition.

Leave a Reply

Your email address will not be published. Required fields are marked *