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:
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.
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:
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,
- 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 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:
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.
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:
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
.
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 src/model
folder, defining the
following classes for error (or exception) types: NoConstraintViolation,
MandatoryValueConstraintViolation, RangeConstraintViolation, IntervalConstraintViolation,
PatternConstraintViolation, UniquenessConstraintViolation, OtherConstraintViolation.
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
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);
}
};
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:
Book.convertRow2Obj
and Book.loadAllInstances
for
loading all managed Book instances from the persistent data store.
Book.saveAllInstances
for saving all managed Book instances to the
persistent data store.
Book.createRow
for creating a new Book instance.
Book.updateRow
for updating an existing Book instance.
Book.deleteRow
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.createRow
,
Book.updateRow
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 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;
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:
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 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);
});
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};
formEl.isbn.setCustomValidity( Book.checkIsbnAsId( slots.isbn).message);
formEl.title.setCustomValidity( Book.checkTitle( slots.title).message);
formEl.year.setCustomValidity( Book.checkYear( slots.year).message);
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;
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;
} 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);
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,