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:
-
Due to the fact that the isbn
attribute is declared to be a standard identifier, it is mandatory and unique.
-
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".
-
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.
-
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:
-
In the model code we have to take care of
- 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,
- performing constraint validation before any new data is saved.
-
In the user interface ("view") code we have to take care of
- styling the user interface with CSS rules,
- responsive validation on user input for providing immediate feedback to the user,
- 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:
-
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:
-
A check operation, such as checkIsbn
, for checking all basic constraints of an identifier attribute, except the mandatory value and the uniqueness constraints.
-
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.
-
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:
-
util.js
contains the definitions of a few utility functions such as isNonEmptyString(x)
for testing if x
is a non-empty string.
-
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
.
-
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):
-
browserShims.js
and util.js
from the lib
folder, discussed in the previous Section,
-
initialize.js
from the src/ctrl
folder, defining the app's MVC namespaces, as discussed in Part 1 (our minimal app tutorial).
-
errorTypes.js
from the lib
folder, defining exception classes.
-
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 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
-
Encode the model class as a JavaScript constructor function.
-
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.
-
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.
-
Encode the add and remove operations, if there are any.
-
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) {
this.isbn = "";
this.title = "";
this.year = 0;
if (arguments.length > 0) {
this.setIsbn( slots.isbn);
this.setTitle( slots.title);
this.setYear( slots.year);
if (slots.edition) this.setEdition( slots.edition);
}
};
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 lib/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 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:
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:
-
Book.convertRow2Obj
and Book.loadAll
for loading all managed Book instances from the persistent data store.
-
Book.saveAll
for saving all managed Book instances to the persistent data store.
-
Book.add
for creating a new Book instance.
-
Book.update
for updating an existing Book instance.
-
Book.destroy
for deleting a Book instance.
-
Book.createTestData
for creating a few example book records to be used as test data.
-
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
-
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
-
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:
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:
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;
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
:
<!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
:
<!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:
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.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
:
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:
-
a function, such as handleSubmitButtonClickEvent
, for handling the event when the user clicks the save/submit button,
-
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:
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);
});
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:
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
};
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);
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:
pl.view.updateBook = {
setupUserInterface: function () {
var formEl = document.forms['Book'],
submitButton = formEl.commit,
selectBookEl = formEl.selectBook;
util.fillWithOptionsFromAssocArray( Book.instances, selectBookEl,
"isbn", "title");
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);
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
};
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 = "";
this.title = "";
this.year = 0;
this.category = 0;
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:
-
Due to the fact that the movieId
attribute is declared to be the standard identifier of Movie
, it is mandatory and unique.
-
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.
-
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
-
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,
-
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
-
styling the user interface with CSS rules (by integrating a CSS library such as Yahoo's Pure CSS),
-
validation on user input for providing immediate feedback to the user,
-
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.