Click here to Skip to main content
15,881,588 members
Articles / Web Development / HTML

JavaScript Front-End Web App Tutorial Part 2: Adding Constraint Validation

Rate me:
Please Sign up or sign in to vote.
4.89/5 (18 votes)
25 Jun 2015CPOL26 min read 46.9K   689   46   2
Learn how to develop front-end web apps with responsive (HTML5) constraint validation using plain JavaScript. Do not use any framework/library (like jQuery or Angular), which just create black-box dependencies and overhead, and prevent you from learning how to do it yourself.

Introduction

This article has been extracted from a five part tutorial about engineering front-end web applications with plain JavaScript available as the open access book Building Front-End Web Apps with Plain JavaScript. It shows how to build a front-end web app with responsive (HTML5) constraint validation using plain JavaScript (no framework or library). If you want to see how it works, you can run the validation app discussed in this article from our server.

A front-end web app can be provided by any web server, but it is executed on the user's computer device (smartphone, tablet or notebook), and not on the remote web server. Typically, but not necessarily, a front-end web application is a single-user application, which is not shared with other users.

The data management app discussed in this tutorial only includes the validation part of the overall functionality required for a complete app. It takes care of only one object type ("books") and supports the four standard data management operations (Create/Read/Update/Delete), but it needs to be enhanced by adding further important parts of the app's overall functionality:

  • Part 3: Dealing with enumerations

  • Part 4: Managing unidirectional associations assigning authors and publishers to books

  • Part 5: Managing bidirectional associations also assigning books to authors and to publishers

  • Part 6: Handling subtype (inheritance) relationships in class hierarchies.

Part 1 of this tutorial is available as the CodeProject article JavaScript Front-End Web App Tutorial Part 1: Building a Minimal App in Seven Steps. The minimal app that we present in Part 1 is limited to support the minimum functionality of a data management app only. For instance, it does not take care of preventing the user from entering invalid data into the app's database. In this second part of the tutorial we show how to express integrity constraints in a JavaScript model class, and how to perform constraint validation both in the model part of the app and in the HTML5 user interface.

Background

If you didn't read it already, you may first want to read Part 1 of this tutorial: Building a Minimal App in Seven Steps.

For a better conceptual understanding of the most important types of integrity constraints, read my CodeProject article Integrity Constraints and Data Validation.

Coding the App

We again consider the single-class data management problem discussed in Part 1 of this tutorial. So, again, the purpose of our app is to manage information about books. But now we also consider the data integrity rules, or integrity constraints, that govern the management of book data. They can be expressed in a UML class model as shown in the following diagram:

Figure 1. An information design model for a single-class app

In this simple model, the following constraints have been expressed:

  1. Due to the fact that the isbn attribute is declared to be a standard identifier, it is mandatory and unique.

  2. The isbn attribute has a pattern constraint requiring its values to match the ISBN-10 format that admits only 10-digit strings or 9-digit strings followed by "X".

  3. The title attribute is mandatory, as indicated by its multiplicity expression [1], and has a string length constraint requiring its values to have at most 50 characters.

  4. The year attribute is mandatory and has an interval constraint, however, of a special form since the maximum is not fixed, but provided by the calendaric function nextYear(), which we implement as a utility function.

Notice that the edition attribute is not mandatory, but optional, as indicated by its multiplicity expression [0..1]. In addition to these constraints, there are the implicit range constraints defined by assigning the datatype NonEmptyString as range to isbn and title, Integer to year, and PositiveInteger to edition. In our plain JavaScript approach, all these property constraints are encoded in the model class within property-specific check functions.

Using the HTML5 Form Validation API

We only use two methods of the HTML5 form validation API for validating constraints in the HTML-forms-based user interface of our app. The first of them, setCustomValidity, allows to mark a form input field as either valid or invalid by assigning either an empty string or a non-empty message to it. The second method, checkValidity, is invoked on a form and tests, if all input fields have a valid value.
Notice that in our approach there is no need to use the new HTML5 attributes for validation, such as required, since we perform all validations with the help of setCustomValidity and our property check functions, as we explain below.

See this Mozilla tutorial or this HTML5Rocks tutorial for more about the HTML5 form validation API.

New Issues

Compared to the minimal app discussed in Part 1, we have to deal with a number of new issues:

  1. In the model code we have to take care of

    1. adding for every property a check function that validates the constraints defined for the property, and a setter method that invokes the check function and is to be used for setting the value of the property,
    2. performing constraint validation before any new data is saved.
  2. In the user interface ("view") code we have to take care of

    1. styling the user interface with CSS rules,
    2. responsive validation on user input for providing immediate feedback to the user,
    3. validation on form submission for preventing the submission of flawed data to the model layer.
    For improving the break-down of the view code, we introduce a utility method (in lib/util.js) that fills a select form control with option elements the contents of which is retrieved from an associative array such as Book.instances. This method is used in the setupUserInterface method of both the updateBook and the deleteBook use cases.

Checking the constraints in the user interface on user input is important for providing immediate feedback to the user. But it is not safe enough to perform constraint validation only in the user interface, because this could be circumvented in a distributed web application where the user interface runs in the web browser of a front-end device while the application's data is managed by a backend component on a remote web server. Consequently, we need a two-fold validation of constraints, first in the user interface, and subsequently in the model code responsible for data storage.

Our solution to this problem is to keep the constraint validation code in special check functions in the model classes and invoke these functions both in the user interface on user input and on form submission, as well as in the add and update data management methods of the model class via invoking the setters. Notice that certain relationship (such as referential integrity) constraints may also be violated through a delete operation, but in our single-class example we don't have to consider this.

Make a JavaScript Data Model

Using the information design model shown in Figure 1 above as the starting point, we make a JavaScript data model by performing the following steps:

  1. Create a check operation for each non-derived property in order to have a central place for implementing all the constraints that have been defined for a property in the design model. For a standard identifier (or primary key) attribute, such as Book::isbn, two check operations are needed:

    1. A check operation, such as checkIsbn, for checking all basic constraints of an identifier attribute, except the mandatory value and the uniqueness constraints.

    2. A check operation, such as checkIsbnAsId, for checking in addition to the basic constraints the mandatory value and uniqueness constraints that are required for an identifier attribute.

    The checkIsbnAsId function is invoked on user input for the isbn form field in the create book form, and also in the setIsbn method, while the checkIsbn function can be used for testing if a value satisfies the syntactic constraints defined for an ISBN.

  2. Create a setter operation for each non-derived single-valued property. In the setter, the corresponding check operation is invoked and the property is only set, if the check does not detect any constraint violation.

This leads to the JavaScript data model shown on the right hand side of the mapping arrow in the following figure:

Figure 2. Deriving a JavaScript data model from an information design model

The JavaScript data model extends the design model by adding checks and setters for each property. Notice that the names of check functions are underlined, since this is the convention in UML for class-level ("static") methods.

Set up the folder structure and create four initial files

The MVC folder structure of our simple app extends the structure of the minimal app by adding two folders, css for adding the CSS file main.css and lib for adding the generic function libraries browserShims.js and util.js. Thus, we end up with the following folder structure containing four initial files:

publicLibrary
  css
    main.css
  lib
    browserShims.js
    errorTypes.js
    util.js
  src
    ctrl
    model
    view
  index.html

We discuss the contents of the five initial files in the following sections.

1. Style the user interface with CSS

We style the UI with the help of the CSS library Pure provided by Yahoo. We only use Pure's basic styles, which include the browser style normalization of normalize.css, and its styles for forms. In addition, we define our own style rules for table and menu elements in main.css.

2. Provide general application-independent code in library files

General application-independent code includes utility functions and JavaScript fixes. We add three files to the lib folder:

  1. util.js contains the definitions of a few utility functions such as isNonEmptyString(x) for testing if x is a non-empty string.

  2. browserShims.js contains a definition of the string trim function for older browsers that don't support this function (which was only added to JavaScript in ECMAScript Edition 5, defined in 2009). More browser shims for other recently defined functions, such as querySelector and classList, could also be added to browserShims.js.

  3. errorTypes.js defines general classes for error (or exception) types: NoConstraintViolation, MandatoryValueConstraintViolation, RangeConstraintViolation, IntervalConstraintViolation, PatternConstraintViolation, UniquenessConstraintViolation, OtherConstraintViolation.

3. Create a start page

The start page of the app first takes care of the styling by loading the Pure CSS base file (from the Yahoo site) and our main.css file with the help of the two link elements (in lines 6 and 7), then it loads the following JavaScript files (in lines 8-12):

  1. browserShims.js and util.js from the lib folder, discussed in the previous Section,

  2. initialize.js from the src/ctrl folder, defining the app's MVC namespaces, as discussed in Part 1 (our minimal app tutorial).

  3. errorTypes.js from the lib folder, defining exception classes.

  4. Book.js from the src/model folder, a model class file that provides data management and other functions.

The app's start page index.html.

HTML
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>JS front-end Validation App Example</title>
    <link rel="stylesheet" type="text/css" 
        href="http://yui.yahooapis.com/combo?pure/0.3.0/base-min.css" />
    <link rel="stylesheet" type="text/css" href="css/main.css" /> 
    <script src="lib/browserShims.js"></script>
    <script src="lib/util.js"></script>
    <script src="lib/errorTypes.js"></script>
    <script src="src/ctrl/initialize.js"></script>
    <script src="src/model/Book.js"></script>
  </head>
  <body>
    <h1>Public Library</h1>
    <h2>Validation Example App</h2>
    <p>This app supports the following operations:</p>
    <menu>
      <li><a href="listBooks.html"><button type="button">List all books</button></a></li>
      <li><a href="createBook.html"><button type="button">Add a new book</button></a></li>
      <li><a href="updateBook.html"><button type="button">Update a book</button></a></li>
      <li><a href="deleteBook.html"><button type="button">Delete a book</button></a></li>
      <li><button type="button" onclick="Book.clearData()">Clear database</button></li>
      <li><button type="button" onclick="Book.createTestData()">Create test data</button></li>
    </menu>
</body>
</html> 

Write the Model Code

How to Encode a JavaScript Data Model

The JavaScript data model shown on the right hand side in Figure 2 can be encoded step by step for getting the code of the model layer of our JavaScript front-end app. These steps are summarized in the following section.

1. Summary

  1. Encode the model class as a JavaScript constructor function.

  2. Encode the check functions, such as checkIsbn or checkTitle, in the form of class-level ('static') methods. Take care that all constraints of the property, as specified in the JavaScript data model, are properly encoded in the check functions.

  3. Encode the setter operations, such as setIsbn or setTitle, as (instance-level) methods. In the setter, the corresponding check operation is invoked and the property is only set, if the check does not detect any constraint violation.

  4. Encode the add and remove operations, if there are any.

  5. Encode any other operation.

These steps are discussed in more detail in the following sections.

2. Encode the model class as a constructor function

The class Book is encoded by means of a corresponding JavaScript constructor function with the same name Book such that all its (non-derived) properties are supplied with values from corresponding key-value slots of the constructor parameter slots.

JavaScript
function Book( slots) {
  // assign default values
  this.isbn = "";   // string
  this.title = "";  // string
  this.year = 0;    // number (int)
  // assign properties only if the constructor is invoked with an argument
  if (arguments.length > 0) {
    this.setIsbn( slots.isbn); 
    this.setTitle( slots.title); 
    this.setYear( slots.year);
    if (slots.edition) this.setEdition( slots.edition);  // optional attribute
  }
};

In the constructor body, we first assign default values to the class properties. These values will be used when the constuctor is invoked as a default constructor (without arguments), or when it is invoked with only some arguments. It is helpful to indicate the range of a property in a comment. This requires to map the platform-independent data types of the information design model to the corresponding implicit JavaScript data types according to the following table.

Platform-independent datatype JavaScript datatype
String string
Integer number (int)
Decimal number (float)
Boolean boolean
Date Date

Since the setters may throw constraint violation errors, the constructor function, and any setter, should be called in a try-catch block where the catch clause takes care of processing errors (at least logging suitable error messages).

As in the minimal app, we add a class-level property Book.instances representing the collection of all Book instances managed by the application in the form of an associative array:

JavaScript
Book.instances = {}; 

3. Encode the property checks

Encode the property check functions in the form of class-level ('static') methods. In JavaScript, this means to define them as function slots of the constructor, as in Book.checkIsbn. Take care that all constraints of a property as specified in the JavaScript data model are properly encoded in its check function. This concerns, in particular, the mandatory value and uniqueness constraints implied by the standard identifier declaration (with «stdid»), and the mandatory value constraints for all properties with multiplicity 1, which is the default when no multiplicity is shown. If any constraint is violated, an error object instantiating one of the error classes listed above and defined in the file lib/errorTypes.js is returned.

For instance, for the checkIsbn operation we obtain the following code:

JavaScript
Book.checkIsbn = function (id) {
  if (!id) {
    return new NoConstraintViolation();
  } else if (typeof(id) !== "string" || id.trim() === "") {
    return new RangeConstraintViolation("The ISBN must be a non-empty string!");
  } else if (!/\b\d{9}(\d|X)\b/.test( id)) {
    return new PatternConstraintViolation(
        'The ISBN must be a 10-digit string or a 9-digit string followed by "X"!');
  } else {
    return new NoConstraintViolation();
  }
};

Notice that, since isbn is the standard identifier attribute of Book, we only check the syntactic constraints in checkIsbn, but we check the mandatory value and uniqueness constraints in checkIsbnAsId, which itself first invokes checkIsbn:

JavaScript
Book.checkIsbnAsId = function (id) {
  var constraintViolation = Book.checkIsbn( id);
  if ((constraintViolation instanceof NoConstraintViolation)) {
    if (!id) {
      constraintViolation = new MandatoryValueConstraintViolation(
          "A value for the ISBN must be provided!");
    } else if (Book.instances[id]) {  
      constraintViolation = new UniquenessConstraintViolation(
          "There is already a book record with this ISBN!");
    } else {
      constraintViolation = new NoConstraintViolation();
    } 
  }
  return constraintViolation;
};

4. Encode the property setters

Encode the setter operations as (instance-level) methods. In the setter, the corresponding check function is invoked and the property is only set, if the check does not detect any constraint violation. Otherwise, the constraint violation error object returned by the check function is thrown. For instance, the setIsbn operation is encoded in the following way:

JavaScript
Book.prototype.setIsbn = function (id) {
  var validationResult = Book.checkIsbnAsId( id);
  if (validationResult instanceof NoConstraintViolation) {
    this.isbn = id;
  } else {
    throw validationResult;
  }
};

There are similar setters for the other properties (title, year and edition).

5. Add a serialization function

It is helpful to have a serialization function tailored to the structure of a class such that the result of serializing an object is a human-readable string representation of the object showing all relevant information items of it. By convention, these functions are called toString(). In the case of the Book class, we use the following code:

JavaScript
Book.prototype.toString = function () {
  return "Book{ ISBN:" + this.isbn + ", title:" + 
      this.title + ", year:" + this.year +"}"; 
};

6. Data management operations

In addition to defining the model class in the form of a constructor function with property definitions, checks and setters, as well as a toString() function, we also need to define the following data management operations as class-level methods of the model class:

  1. Book.convertRow2Obj and Book.loadAll for loading all managed Book instances from the persistent data store.

  2. Book.saveAll for saving all managed Book instances to the persistent data store.

  3. Book.add for creating a new Book instance.

  4. Book.update for updating an existing Book instance.

  5. Book.destroy for deleting a Book instance.

  6. Book.createTestData for creating a few example book records to be used as test data.

  7. Book.clearData for clearing the book datastore.

All of these methods essentially have the same code as in our minimal app discussed in Part 1, except that now

  1. we may have to catch constraint violations in suitable try-catch blocks in the procedures Book.convertRow2Obj, Book.add, Book.update and Book.createTestData; and

  2. we can use the toString() function for serializing an object in status and error messages.

Notice that for the change operations add and update, we need to implement an all-or-nothing policy: as soon as there is a constraint violation for a property, no new object must be created and no (partial) update of the affected object must be performed.

When a constraint violation is detected in one of the setters called when new Book(...) is invoked in Book.add, the object creation attempt fails, and instead a constraint violation error message is created in line 6. Otherwise, the new book object is added to Book.instances and a status message is creatred in lines 10 and 11, as shown in the following program listing:

JavaScript
Book.add = function (slots) {
  var book = null;
  try {
    book = new Book( slots);
  } catch (e) {
    console.log( e.name +": "+ e.message);
    book = null;
  }
  if (book) {
    Book.instances[book.isbn] = book;
    console.log( book.toString() + " created!");
  }
};

Likewise, when a constraint violation is detected in one of the setters invoked in Book.update, a constraint violation error message is created (in line 16) and the previous state of the object is restored (in line 19). Otherwise, a status message is created (in lines 23 or 25), as shown in the following program listing:

JavaScript
Book.update = function (slots) {
  var book = Book.instances[slots.isbn],
      noConstraintViolated = true,
      updatedProperties = [],
      objectBeforeUpdate = util.cloneObject( book);
  try {
    if (book.title !== slots.title) {
      book.setTitle( slots.title);
      updatedProperties.push("title");
    }
    if (book.year !== parseInt( slots.year)) {
      book.setYear( slots.year);
      updatedProperties.push("year");
    }
    if (slots.edition && book.edition !== parseInt(slots.edition)) {
        book.setEdition( slots.edition);
        updatedProperties.push("edition");
    }
  } catch (e) {
    console.log( e.name +": "+ e.message);
    noConstraintViolated = false;
    // restore object to its state before updating
    Book.instances[slots.isbn] = objectBeforeUpdate;
  }
  if (noConstraintViolated) {
    if (updatedProperties.length > 0) {
      console.log("Properties " + updatedProperties.toString() +
          " modified for book " + slots.isbn);
    } else {
      console.log("No property value changed for book " + slots.isbn + " !");
    }
  }
};

The View and Controller Layers

The user interface (UI) consists of a start page index.html that allows the user choosing one of the data management operations by navigating to the corresponding UI page such as listBooks.html or createBook.html in the app folder. The start page index.html has been discussed above.

After loading the Pure base stylesheet and our own CSS settings in main.css, we first load some browser shims and utility functions. Then we initialize the app in src/ctrl/initialize.js and continue loading the error classes defined in lib/errorTypes.js and the model class Book.

We render the data management menu items in the form of buttons. For simplicity, we invoke the Book.clearData() and Book.createTestData() methods directly from the buttons' onclick event handler attribute. Notice, however, that it is generally preferable to register such event handling functions with addEventListener(...), as we do in all other cases.

1. The data management UI pages

Each data management UI page loads the same basic CSS and JavaScript files like the start page index.html discussed above. In addition, it loads two use-case-specific view and controller files src/view/useCase.js and src/ctrl/useCase.js and then adds a use case initialize function (such as pl.ctrl.listBooks.initialize) as an event listener for the page load event, which takes care of initializing the use case when the UI page has been loaded.

For the "list books" use case, we get the following code in listBooks.html:

HTML
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>JS front-end Validation App Example</title>
  <link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.3.0/pure-min.css" />
  <link rel="stylesheet" href="css/main.css" /> 
  <script src="lib/browserShims.js"></script>
  <script src="lib/util.js"></script>
  <script src="lib/errorTypes.js"></script>
  <script src="src/ctrl/initialize.js"></script>
  <script src="src/model/Book.js"></script>
  <script src="src/view/listBooks.js"></script>
  <script src="src/ctrl/listBooks.js"></script>
  <script>
    window.addEventListener("load", pl.ctrl.listBooks.initialize);
  </script>
</head>
<body>
  <h1>Public Library: List all books</h1>
  <table id="books">
    <thead>
      <tr><th>ISBN</th><th>Title</th><th>Year</th><th>Edition</th></tr>
    </thead>
    <tbody></tbody>
  </table>
  <nav><a href="index.html">Back to main menu</a></nav>
</body>
</html>

For the "create book" use case, we get the following code in createBook.html:

HTML
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>JS front-end Validation App Example</title>
  <link rel="stylesheet" href="http://yui.yahooapis.com/combo?pure/0.3.0/base-
       min.css&pure/0.3.0/forms-min.css" />
  <link rel="stylesheet" href="css/main.css" /> 
  <script src="lib/browserShims.js"></script>
  <script src="lib/util.js"></script>
  <script src="lib/errorTypes.js"></script>
  <script src="src/ctrl/initialize.js"></script>
  <script src="src/model/Book.js"></script>
  <script src="src/view/createBook.js"></script>
  <script src="src/ctrl/createBook.js"></script>
  <script>
    window.addEventListener("load", pl.ctrl.createBook.initialize);
  </script>
  </head>
  <body>
  <h1>Public Library: Create a new book record</h1>
  <form id="Book" class="pure-form pure-form-aligned">
    <div class="pure-control-group">
      <label for="isbn">ISBN</label>
      <input id="isbn" name="isbn" />
    </div>
    <div class="pure-control-group">
      <label for="title">Title</label>
      <input id="title" name="title" />
    </div>
    <div class="pure-control-group">
      <label for="year">Year</label>
      <input id="year" name="year" />
    </div>
    <div class="pure-control-group">
      <label for="edition">Edition</label>
      <input id="edition" name="edition" />
    </div>
    <div class="pure-controls">
      <p><button type="submit" name="commit">Save</button></p>
      <nav><a href="index.html">Back to main menu</a></nav>
    </div>
  </form>
</body>
</html>

Notice that for styling the form elements in createBook.html, and also for updateBook.html and deleteBook.html, we use the Pure CSS form styles. This requires to assign specific values, such as "pure-control-group", to the class attributes of the form's div elements containing the form controls. We have to use explicit labeling (with the label element's for attribute referencing the input element's id), since Pure does not support implicit labels where the label element contains the input element.

2. Initialize the app

For initializing the app, its namespace and MVC subnamespaces have to be defined. For our example app, the main namespace is defined to be pl, standing for "Public Library", with the three subnamespaces model, view and ctrl being initially empty objects:

JavaScript
var pl = { model:{}, view:{}, ctrl:{} }; 

We put this code in the file initialize.js in the ctrl folder.

3. Initialize the data management use cases

For initializing a data management use case, the required data has to be loaded from persistent storage and the UI has to be set up. This is performed with the help of the controller procedures pl.ctrl.createBook.initialize and pl.ctrl.createBook.loadData defined in the controller file ctrl/createBook.js with the following code:

JavaScript
pl.ctrl.createBook = {
  initialize: function () {
    pl.ctrl.createBook.loadData();
    pl.view.createBook.setupUserInterface();
  },
  loadData: function () {
    Book.loadAll();
  }
};

All other data management use cases (read/list, update, delete) are handled in the same way.

4. Set up the user interface

For setting up the user interfaces of the data management use cases, we have to distinguish the case of "list books" from the other ones (create, update, delete). While the latter ones require using an HTML form and attaching event handlers to form controls, in the case of "list books" we only have to render a table displaying all the books, as shown in the following program listing of view/listBooks.js:

JavaScript
pl.view.listBooks = {
  setupUserInterface: function () {
    var tableBodyEl = document.querySelector("table#books>tbody");
    var i=0, book=null, row={}, key="", keys = Object.keys( Book.instances);
    for (i=0; i < keys.length; i++) {
      key = keys[i];
      book = Book.instances[key];
      row = tableBodyEl.insertRow(-1);
      row.insertCell(-1).textContent = book.isbn;
      row.insertCell(-1).textContent = book.title;
      row.insertCell(-1).textContent = book.year;
      if (book.edition) row.insertCell(-1).textContent = book.edition;
    }
  }
};

For the create, update and delete use cases, we need to attach the following event handlers to form controls:

  1. a function, such as handleSubmitButtonClickEvent, for handling the event when the user clicks the save/submit button,

  2. functions for validating the data entered by the user in form fields (if there are any).

In addition, in the following view/createBook.js code, we add an event handler for saving the application data in the case of a beforeunload event, which occurs, for instance, when the browser (or browser tab) is closed:

JavaScript
pl.view.createBook = {
  setupUserInterface: function () {
    var formEl = document.forms['Book'],
        submitButton = formEl.commit;
    submitButton.addEventListener("click", this.handleSubmitButtonClickEvent);
    formEl.isbn.addEventListener("input", function () { 
        formEl.isbn.setCustomValidity( Book.checkIsbnAsId( formEl.isbn.value).message);
    });
    formEl.title.addEventListener("input", function () { 
        formEl.title.setCustomValidity( Book.checkTitle( formEl.title.value).message);
    });
    formEl.year.addEventListener("input", function () { 
        formEl.year.setCustomValidity( Book.checkYear( formEl.year.value).message);
    });
    formEl.edition.addEventListener("input", function () {
        formEl.edition.setCustomValidity(
            Book.checkEdition( formEl.edition.value).message);
    });
    // neutralize the submit event
    formEl.addEventListener( 'submit', function (e) { 
        e.preventDefault();;
        formEl.reset();
    });
    window.addEventListener("beforeunload", function () {
        Book.saveAll(); 
    });
  },
  handleSubmitButtonClickEvent: function () {
    ...
  }
}; 

Notice that for each form input field we add a listener for input events, such that on any user input a validation check is performed because input events are created by user input actions such as typing. We use the predefined function setCustomValidity from the HTML5 form validation API for having our property check functions invoked on the current value of the form field and returning an error message in the case of a constraint violation. So, whenever the string represented by the expression Book.checkIsbn( formEl.isbn.value).message is empty, everything is fine. Otherwise, if it represents an error message, the browser indicates the constraint violation to the user by rendering a red outline for the form field concerned (due to our CSS rule for the :invalid pseudo class).

While the validation on user input enhances the usability of the UI by providing immediate feedback to the user, validation on form data submission is even more important for catching invalid data. Therefore, the event handler handleSaveButtonClickEvent() performs the property checks again with the help of setCustomValidity, as shown in the following program listing:

JavaScript
handleSubmitButtonClickEvent: function () {
  var formEl = document.forms['Book'];
  var slots = { isbn: formEl.isbn.value,
          title: formEl.title.value,
          year: formEl.year.value,
          edition: formEl.edition.value
  };
  // set error messages in case of constraint violations
  formEl.isbn.setCustomValidity( Book.checkIsbnAsId( slots.isbn).message);
  formEl.title.setCustomValidity( Book.checkTitle( slots.title).message);
  formEl.year.setCustomValidity( Book.checkYear( slots.year).message);
  formEl.edition.setCustomValidity(
      Book.checkEdition( formEl.edition.value).message);
  // save the input data only if all of the form fields are valid
  if (formEl.checkValidity()) {
    Book.create( slots);
  }
}

By invoking checkValidity() we make sure that the form data is only saved (by Book.create), if there is no constraint violation. After this handleSubmitButtonClickEvent handler has been executed on an invalid form, the browser takes control and tests if the pre-defined property validity has an error flag for any form field. In our approach, since we use setCustomValidity, the validity.customError would be true. If this is the case, the custom constraint violation message will be displayed (in a bubble) and the submit event will be suppressed.

For the use case update book, which is handled in view/updateBook.js, we provide a book selection list, so the user need not enter an identifier for books (an ISBN), but has to select the book to be updated. This implies that there is no need to validate the ISBN form field, but only the title and year fields. We get the following code:

JavaScript
pl.view.updateBook = {
  setupUserInterface: function () {
    var formEl = document.forms['Book'],
        submitButton = formEl.commit,
        selectBookEl = formEl.selectBook;
    // set up the book selection list
    util.fillWithOptionsFromAssocArray( Book.instances, selectBookEl, 
        "isbn", "title");
    // when a book is selected, populate the form with its data
    selectBookEl.addEventListener("change", function () {
      var bookKey = selectBookEl.value;
      if (bookKey) {
        book = Book.instances[bookKey];
        formEl.isbn.value = book.isbn;
        formEl.title.value = book.title;
        formEl.year.value = book.year;
        if (book.edition) formEl.edition.value = book.edition;
      } else {
        formEl.reset();
      }
    });
    formEl.title.addEventListener("input", function () { 
        formEl.title.setCustomValidity( 
            Book.checkTitle( formEl.title.value).message);
    });
    formEl.year.addEventListener("input", function () { 
        formEl.year.setCustomValidity( 
            Book.checkYear( formEl.year.value).message);
    });
    formEl.edition.addEventListener("input", function () {
        formEl.edition.setCustomValidity(
            Book.checkEdition( formEl.edition.value).message);
    });
    saveButton.addEventListener("click", this.handleSubmitButtonClickEvent);
    // neutralize the submit event
    formEl.addEventListener( 'submit', function (e) { 
        e.preventDefault();;
        formEl.reset();
    });
    window.addEventListener("beforeunload", function () {
        Book.saveAll(); 
    });
  },

When the save button on the update book form is clicked, the title and year form field values are validated by invoking setCustomValidity, and then the book record is updated if the form data validity can be established with checkValidity:

  handleSubmitButtonClickEvent: function () {
    var formEl = document.forms['Book'];
    var slots = { isbn: formEl.isbn.value,
        title: formEl.title.value,
        year: formEl.year.value,
        edition: formEl.edition.value
    };
    // set error messages in case of constraint violations
    formEl.title.setCustomValidity( Book.checkTitle( slots.title).message);
    formEl.year.setCustomValidity( Book.checkYear( slots.year).message);
    formEl.edition.setCustomValidity(
            Book.checkEdition( formEl.edition.value).message);
    if (formEl.checkValidity()) {
      Book.update( slots);
    }
  } 

The logic of the setupUserInterface methods for the delete use case is similar.

Run the App

You can run the validation app from our server, and find more resources about web engineering, including open access books, on web-engineering.info.

Possible Variations and Extensions

Simplifying forms with implicit labels

The explicit labeling of form fields used in this tutorial requires to add an id value to the input element and a for-reference to its label element as in the following example:

<div class="pure-control-group">
  <label for="isbn">ISBN:</label>
  <input id="isbn" name="isbn" />
</div>

This technique for associating a label with a form field is getting quite inconvenient when we have many form fields on a page because we have to make up a great many of unique id values and have to make sure that they don't conflict with any of the id values of other elements on the same page. It's therefore preferable to use an approach, called implicit labeling, that does not need all these id references. In this approach we make the input element a child element of its label element, as in

<div>
  <label>ISBN: <input name="isbn" /></label>
</div>

Having input as a child of its label doesn't seem very logical (rather, one would expect the label to be a child of an input element). But that's the way, it is defined in HTML5.

A small disadvantage of using implicit labels is the lack of support by popular CSS libraries, such as Pure CSS. In the following parts of this tutorial, we will use our own CSS styling for implicitly labeled form fields.

Dealing with enumeration attributes

In all application domains, there are enumeration datatypes that define the possible values of enumeration attributes. For instance, when we have to manage data about persons, we often need to include information about the gender of a person. The possible values of a gender attribute are restricted to one of the following: "male","female", or "undetermined". Instead of using these strings as the internal values of the enumeration attribute gender, it is preferable to use the positive integers 1, 2 and 3, which enumerate the possible values. However, since these integers do not reveal their meaning (the enumeration label they stand for) in program code, for readability we rather use special constants, called enumeration literals, such as GenderEL.MALE and GenderEL.FEMALE, in program statements like this.gender = GenderEL.FEMALE. Notice that, by convention, enumeration literals are all upper case.

We can implement an enumeration in the form of a special JavaScript object definition using the Object.defineProperties method:

var BookCategoryEL = null;
Object.defineProperties( BookCategoryEL, {
  NOVEL: {value: 1, writable: false},
  BIOGRAPHY: {value: 2, writable: false},
  TEXTBOOK: {value: 3, writable: false},
  OTHER: {value: 4, writable: false},
  MAX: {value: 4, writable: false},
  labels: {value:["novel","biography","textbook","other"], writable: false}
});

Notice how this definition of an enumeration of book categories takes care of the requirement that enumeration literals like BookCategoryEL.NOVEL are constants, the value of which cannot be changed during program execution. This is achieved with the help of the property descriptor writable: false in the Object.defineProperties statement.

This definition allows using the enumeration literals BookCategoryEL.NOVEL, BookCategoryEL.BIOGRAPHY etc., standing for the enumeration integers 1, 2 , 3 and 4, in program statements. Notice that we use the convention to suffix the name of an enumeration with "EL" standing for "enumeration literal".

Having an enumeration like BookCategoryEL, we can then check if an enumeration attribute like category has an admissible value by testing if its value is not smaller than 1 and not greater than BookCategoryEL.MAX.

We consider the following model class Book with the enumeration attribute category:

function Book( slots) {
  this.isbn = "";     // string
  this.title = "";    // string
  this.year = 0;      // number (int)
  this.category = 0;  // number (enum)
  if (arguments.length > 0) {
    this.setIsbn( slots.isbn); 
    this.setTitle( slots.title); 
    this.setYear( slots.year);
    this.setCategory( slots.category);
  }
};

For validating input values for the enumeration attribute category, we can use the following check function:

Book.checkCategory = function (c) {
  if (!c) {
    return new MandatoryValueConstraintViolation("A category must be provided!");
  } else if (!util.isPositiveInteger(c) || c > BookCategoryEL.MAX) {
    return new RangeConstraintViolation("The category must be a positive integer "+
                 "not greater than "+ BookCategoryEL.MAX +" !");
  } else {
    return new NoConstraintViolation();
  }
};

Notice how the range constraint defined by the enumeration BookCategoryEL is checked: it is tested if the input value c is a positive integer and if it is not greater than BookCategoryEL.MAX.

In the user interface, an output field for an enumeration attribute would display the enumeration label, rather than the enumeration integer. The label can be retrieved in the following way:

formEl.category.value = BookCategoryEL.labels[this.category];

For user input to a single-valued enumeration attribute like Book::category, a radio button group could be used if the number of enumeration literals is sufficiently small, otherwise a single selection list would be used. If the selection list is implemented with an HTML select element, the enumeration labels would be used as the text content of the option elements, while the enumeration integers would be used as their values.

For user input to a multi-valued enumeration attribute, a checkbox group could be used if the number of enumeration literals is sufficiently small, otherwise a multiple selection list would be used. For usability, the multiple selection list can only be implemented with an HTML select element, if the number of enumeration literals does not exceed a certain threshold, which depends on the number of options the user can see on the screen without scrolling.

Points of Attention

The reader may have noticed the repetitive code structures (called boilerplate code) needed in the model layer per class and per property for constraint validation (checks and setters) and per class for the data storage management methods add, update, etc. While it is good to write this code a few times for learning app development, you don't want to write it again and again later when you work on real projects. In another article, Declarative and Responsive Constraint Validation with mODELcLASSjs, we present an approach how to put these methods in a generic form in a meta-class, such that they can be reused for all classes of an app.

Practice Project

The purpose of the app to be built is managing information about movies. Like in the book data management app discussed in the tutorial, you can make the simplifying assumption that all the data can be kept in main memory. Persistent data storage is implemented with JavaScript's Local Storage API.

The app deals with just one object type, Movie, as depicted in the class diagram below.

In this model, the following constraints have been expressed:

  1. Due to the fact that the movieId attribute is declared to be the standard identifier of Movie, it is mandatory and unique.

  2. The title attribute is mandatory, as indicated by its multiplicity expression [1], and has a string length constraint requiring its values to have at most 120 characters.

  3. The releaseDate attribute has an interval constraint: it must be greater than or equal to 1895-12-28.

Notice that the releaseDate attribute is not mandatory, but optional, as indicated by its multiplicity expression [0..1]. In addition to the constraints described in this list, there are the implicit range constraints defined by assigning the datatype PositiveInteger to movieId, NonEmptyString to title, and Date to releaseDate. In our plain JavaScript approach, all these property constraints are encoded in the model class within property-specific check functions.

Following the tutorial, you have to to take care of

  1. adding for every property a check function that validates the constraints defined for the property, and a setter method that invokes the check function and is to be used for setting the value of the property,

  2. performing validation before any data is saved in the Movie.add and Movie.update methods.

in the model code of your app, while In the user interface ("view") code you have to take care of

  1. styling the user interface with CSS rules (by integrating a CSS library such as Yahoo's Pure CSS),

  2. validation on user input for providing immediate feedback to the user,

  3. validation on form submission for preventing the submission of invalid data.

Also you have to make sure that your pages comply with the XML syntax of HTML5, and that your JavaScript code complies with our Coding Guidelines and is checked with JSLint (http://www.jslint.com).

If you have any question about this project, you can post them below, in the comments section.

History

  • 24 June 2015, added section "Practice Project"
  • 18 February 2015, corrected the CodeProject "Subsection"
  • 12 February 2015, added new section "Possible Variations and Extensions"
  • 13 October 2014, added a string length constraint example and an optional attribute for illustrating the difference between mandatory and optional attributes.
  • 15 April 2014, updated code, now using submit buttons for having the browser display custom constraint violation messages in bubbles, adding a CSS rule for the :invalid pseudo class, added explanations in Section "New Issues", simplified program listing for updateBook due to using a utility method for filling a select form control with options,
  • 11 April 2014, first version created.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Instructor / Trainer
Germany Germany
Researcher, developer, instructor and cat lover.

Co-Founder of web-engineering.info and the educational simulation website sim4edu.com.

Comments and Discussions

 
QuestionBroken link Pin
Leonid Fofanov20-Feb-15 10:29
Leonid Fofanov20-Feb-15 10:29 
AnswerRe: Broken link Pin
Gerd Wagner22-Feb-15 10:44
professionalGerd Wagner22-Feb-15 10:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.