5. Encode our Web Application with Validation

The essential methods of constraining and validating user-inputs, and three different ways to display error-messages we have discussed above. Now turn our attention to the public library.

5.1. New Folder's Structure with errorMessages

publicLibrary
|-- index.html
|-- js
|   |-- app.js
|   |-- controllers.js
|   `-- directives.js
|-- partials
|   |-- createBook.html
|   |-- deleteBook.html
|   |-- errorMessages
|   |   |-- errorEdition.html
|   |   |-- errorIsbn.html
|   |   |-- errorTitle.html
|   |   `-- errorYear.html
|   |-- main.html
|   |-- showAllBooks.html
|   `-- updateBook.html
`-- src
    |-- AngularJS-route.js
    `-- angular.js

Compared to minimal app the structure have been changed a bit. We mentioned that the file directives.js is used to store user-defined directives and we use ngMessages to display error-messages, in the folder errorMessages there are errorIsbn.html, errorTitle.html, errorYear.html and errorEdition.html to pack error-messages for each sort of input.

Let's now go deep to the files which should be modified.

5.2. Start page - Index.html

For the start page we need to include the two new files directives.js and AngularJS-messages.js to <head>, so the whole index.html will be:

<!DOCTYPE html>
<html ng-app="publicLibraryApp">
<head>
  <title>Public Library with validation</title>
  <script src="src/angular.js"></script>
  <script src="src/AngularJS-route.js"></script>
  <script src="src/AngularJS-messages.js"></script>
  <script src="js/app.js"></script>
  <script src="js/controllers.js"></script>
  <script src="js/directives.js"></script>
</head>
<body>
  <div ng-view="ng-view"></div>
</body>
</html>

5.3. Application's Initialization - js/app.js

We should inject two new modules ngMessages and plDirectives to the main module and define a global variable currentYear within $rootScope:

/* App Module */

var publicLibraryApp = angular.module('publicLibraryApp',
  [
    'ngRoute',
    'ngMessages',  // new module
    'plControllers',
    'plDirectives'  // new module
  ]
);

publicLibraryApp.config(...);

publicLibraryApp.run(['$rootScope', function($rootScope) {
  $rootScope.currentYear = new Date().getFullYear();
}]);

5.4. Create Object Use Case

A big change for template side cause we add for each book's property some necessary constraints, validations and error-messages, but all of the changes are based on the Angular form validation API, generic/specific constraints and validations which discussed in the previous sections. So the file partials/createBook.html should be:

<h1>Public Library: Create a new book record</h1>
<form name="bookInfo" novalidate="novalidate" ng-submit="addBook()">
  <div>
    <label>ISBN:
      <input type="text" name="isbn" ng-model="book.isbn"
             ng-pattern="/\b\d{9}(\d|X)\b/" pl-stdid="pl-stdid"
             ng-required="true" />
    </label>
    <span ng-if="bookInfo.$submitted || bookInfo.isbn.$dirty"
          ng-messages="bookInfo.isbn.$error"
          ng-messages-include="partials/errorMessages/errorIsbn.html">
    </span>
  </div>
  <div>
    <label>Title:
      <input type="text" name="title" ng-model="book.title"
             ng-maxlength="50" ng-required="true" />
    </label>
    <span ng-if="bookInfo.$submitted || bookInfo.title.$dirty"
          ng-messages="bookInfo.title.$error"
          ng-messages-include="partials/errorMessages/errorTitle.html">
    </span>
  </div>
  <div>
    <label>Year:
      <input type="number" name="year" ng-model="book.year"
             ng-min="1459" ng-max="{{currentYear}}" ng-required="true" />
    </label>
    <span ng-if="bookInfo.$submitted || bookInfo.year.$dirty"
          ng-messages="bookInfo.year.$error"
          ng-messages-include="partials/errorMessages/errorYear.html">
    </span>
  </div>
  <div>
    <label>Edition:
      <input type="number" name="edition" ng-model="book.edition"
             ng-min="1" />
    </label>
    <span ng-if="bookInfo.$submitted || bookInfo.edition.$dirty"
          ng-messages="bookInfo.edition.$error"
          ng-messages-include="partials/errorMessages/errorEdition.html">
    </span>
  </div>
  <div>
    <button type="submit" name="commit" ng-disabled="bookInfo.$invalid">
      Save
    </button>
  </div>
</form>

By displaying error-messages we combine ngIf and ngMessages together just for hiding the error-messages before user write something in the input-fields, or when users who use "Web-Developer Tools" change the attribute of submitt-button "disable" to "enable", the error-messages will be shown after user click the submitt button.

The user-defined directive plStdid we had discussed above in Figure 6.2, “User-Defined Direcitve plStdid”.

5.5. Error Messages

Error-Messages for the isbn in partials/errorMessages/errorIsbn.html:

<span ng-message="required">
  A value for the ISBN must be provided!
</span>
<span ng-message="pattern">
  The ISBN must be a 10-digit string or a 9-digit string followed by 'X'.
</span>
<span ng-message="plStdid">
  There is already a book record with this ISBN!
</span>

Error-Messages for the title in partials/errorMessages/errorTitle.html:

<span ng-message="required">
  A title must be provided!
</span>
<span ng-message="maxlength">
  The maximal length of a title is 50!
</span>

Error-Messages for the year in partials/errorMessages/errorYear.html:

<span ng-message="required">
  A publication year must be provided!
</span>
<span ng-message="number">
  The value of year must be an integer!
</span>
<span ng-message="min">
  The value of year must be between 1459 and {{currentYear}}!
</span>
<span ng-message="max">
  The value of year must be between 1459 and {{currentYear}}!
</span>

Error-Messages for the edition in partials/errorMessages/errorEdition.html:

<span ng-message="number">
  The value of edition must be an integer!
</span>
<span ng-message="min">
  The edition must be a positive integer!
</span>

5.6. Update Object Use Case

The process of defining constraints and validation for Update Object use case (partials/updateBook.html) is very similar like for the Create Object use case, but we will add a new Angular directive ngChange combined with a function selectBook(), which is defined in controller, as an attribute of select element, so that everytime when user selects a book, a special event will be triggered. We will discuss it in next section.

5.7. Validation in Controller

Most of jobs for setting constraints and validations and displaying error-messages are finished. But in some cases, user can skip the validation using "Web-Develope Tools" to upload invalid data to our database. For example, when a user want to create a new book, after deleting the submit button's attribute disabled="disabled", the Save bottun will be able to click and addBook() function can be triggered even if the inputs are incorrect. It means we need to validate those inputs again in the controllers before uploading data.

By Angular Forms Validation API there is a build-in property for form bookInfo.$valid in our case to check the whole input data which are saved in $scope.book, that means if all of the input data are correct, it's value would be true. So we should modify a part of the codes in controllers.js, that insert an if statement to the functions addBook() before uploading data, and refer the job to Angular itself:

$scope.addBook = function() {
  if ($scope.bookInfo.$valid) {
    // same codes as in MinimalApp
    ...
  } else {
    console.log("Form is invalid!");
  }
};

We should also valid the inputs for updating event. Except normal validation like if($scope.bookInfo.$valid), we should also consider, if nothing has been changed, this book infromation should not be updated:

var tempBook = {
  isbn: "",
  title: "",
  year: 0,
  edition: 0
};

$scope.selectBook = function () {
  $scope.isSelected = true;
  tempBook = angular.copy( $scope.book);
  $scope.$watchCollection('book', function ( newBook) {
    if ( angular.equals( newBook, tempBook)) {
      $scope.bookInfo.$setPristine();
    }
  });
};

$scope.updateBook = function() {
  if ( $scope.bookInfo.$valid &&
       !angular.equals( $scope.book, tempBook)) {
    // same codes as in MinimalApp
    ...
  } else {
    console.log("Form is invalid or you didn't change any value!");
  }
};

First, we set up a temporary variable tempBook, which is used for saving the original book information $scope.book after one book is selected.

We create a new function selectBook(). Onec a book is selected we will invoke an Angular $scope build-in method $watchCollection to keep our eyes on the properties of the $scope.book object. The first property book is $scope.book itself and the function would be triggered whenever the book is changed. So, a book record can be updated as long as any value has been changed and the changed value is at the same time valid.