Click here to Skip to main content
Click here to Skip to main content

JavaScript Frontend Web App Tutorial Part 2: Adding Constraint Validation

, 15 Apr 2014
Rate this:
Please Sign up or sign in to vote.
A tutorial about developing a single-class frontend web application with constraint validation using plain JavaScript

Introduction

This article is Part 2 of a five part tutorial about engineering frontend web applications with plain JavaScript. It shows how to build a single-class web app with constraint validation using plain JavaScript (without any third-party framework or library). A frontend 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 frontend 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: Managing unidirectional associations

  • Part 4: Managing bidirectional associations

  • Part 5: Handling subtype (inheritance) relationships between object types.

Part 1 of this tutorial is available as the CodeProject article JavaScript Frontend 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.

  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.

In addition to these constraints, there are the implicit range constraints defined by assigning the datatype NonEmptyString as range to isbn and title, and Integer to year. 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. 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 frontend 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 create 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.

  3. Create add and remove operations for each non-derived multi-valued property (if there are any).

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
    util.js
  src
    ctrl
    model
    view
  index.html

We discuss the contents of the four 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 utility functions and JavaScript fixes in library files

We add two library 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. 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 src/model folder, defining the following classes for error (or exception) types: NoConstraintViolation, MandatoryValueConstraintViolation, RangeConstraintViolation, IntervalConstraintViolation, PatternConstraintViolation, UniquenessConstraintViolation, OtherConstraintViolation.

  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.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>JS Frontend 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="src/ctrl/initialize.js"></script>
    <script src="src/model/errorTypes.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 frontend 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.

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); 
  }
};

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:

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 model/errorTypes.js is returned.

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

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:

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:

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 properties title and year.

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:

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.loadAllInstances for loading all managed Book instances from the persistent data store.

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

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

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

  5. Book.deleteRow 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.createRow, Book.updateRow 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 createRow and updateRow, 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.createRow, 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:

Book.createRow = 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.updateRow, 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:

Book.updateRow = 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");
    }
  } 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 src/model/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:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>JS Frontend 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="src/ctrl/initialize.js"></script>
  <script src="src/model/errorTypes.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></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:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>JS Frontend 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="src/ctrl/initialize.js"></script>
  <script src="src/model/errorTypes.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-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:

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:

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

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:

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

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 line 20 of 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:

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);
    });
    // neutralize the submit event
    formEl.addEventListener( 'submit', function (e) { 
        e.preventDefault();;
        formEl.reset();
    });
    window.addEventListener("beforeunload", function () {
        Book.saveAllInstances(); 
    });
  },
  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:

handleSubmitButtonClickEvent: function () {
  var formEl = document.forms['Book'];
  var slots = { isbn: formEl.isbn.value, 
                title: formEl.title.value, 
                year: formEl.year.value};
  // check all input fields and create 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);
  // save the input data only if all of the form fields are valid
  if (formEl.checkValidity()) {
    Book.createRow( slots);
  }
}

By invoking checkValidity() we make sure that the form data is only saved (by Book.createRow), 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:

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;
      } else {
        formEl.isbn.value = "";
        formEl.title.value = "";
        formEl.year.value = "";
      }
    });
    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);
    });
    saveButton.addEventListener("click", this.handleSubmitButtonClickEvent);
    // neutralize the submit event
    formEl.addEventListener( 'submit', function (e) { 
        e.preventDefault();;
        formEl.reset();
    });
    window.addEventListener("beforeunload", function () {
        Book.saveAllInstances(); 
    });
  },

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};
    formEl.title.setCustomValidity( Book.checkTitle( slots.title).message);
    formEl.year.setCustomValidity( Book.checkYear( slots.year).message);
    if (formEl.checkValidity()) {
      Book.updateRow( 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.

History

  • 11 April 2014, first version created.
  • 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,

License

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

Share

About the Author

Gerd Wagner
Instructor / Trainer
Germany Germany
Researcher, developer, instructor and cat lover.
Follow on   Google+   LinkedIn

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web02 | 2.8.140827.1 | Last Updated 15 Apr 2014
Article Copyright 2014 by Gerd Wagner
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid