Building Frontend Web Applications with Plain JavaScript

An incremental tutorial in four parts

Gerd Wagner

Warning: This tutorial manuscript is still in beta stage, so it may still contain errors and may still be incomplete in certain respects. Please report any issue to Gerd Wagner at G.Wagner@b-tu.de.

This tutorial is also available in the following formats: PDF. See also the project page.

Permission is granted to copy, distribute, modify and re-publish this document under the terms of the Creative Commons Attribution, Non Commercial - Share Alike 4.0 license.

12 February 2014

Revision History
20140221gw
create first version
20140212gw
incorporate feedback by Sorin Carbunaru

Acknowledgements

Thanks to Sorin Carbunaru for proof-reading the first version of this tutorial.

Table of Contents

Foreword
I. Get Started
1. Foundations
1. The World Wide Web (WWW)
2. HTML and XML
2.1. XML documents
2.2. Unicode and UTF-8
2.3. XML namespaces
2.4. Correct XML documents
2.5. The evolution of HTML
2.6. HTML forms
3. JavaScript
3.1. Types and data literals in JavaScript
3.2. Different kinds of objects
3.3. Arrays
3.4. Associative arrays
3.5. Defining and instantiating a class
3.6. Class hierarchies
3.7. Enumeration datatypes
3.8. JavaScript as an object-oriented language
Glossary
2. Building a Minimal JavaScript Frontend App in Seven Steps
1. Step 1 - Set up the Folder Structure
2. Step 2 - Write the Model Code
2.1. Representing the collection of all Book instances
2.2. Loading all Book instances
2.3. Saving all Book instances
2.4. Creating a new Book instance
2.5. Updating an existing Book instance
2.6. Deleting an existing Book instance
2.7. Creating test data
2.8. Clearing all data
3. Step 3 - Initialize the Application
4. Step 4 - Implement the List Objects Use Case
5. Step 5 - Implement the Create Object Use Case
6. Step 6 - Implement the Upate Object Use Case
7. Step 7 - Implement the Delete Object Use Case
8. Run the App and Get the Code
II. Integrity Constraints
3. Integrity Constraints and Data Validation
1. Mandatory Value Constraints
2. Range Constraints
3. Interval Constraints
4. Pattern Constraints
5. Uniqueness Constraints
6. Standard Identifiers (Primary Keys)
7. Referential Integrity Constraints
8. Constraints and Validation in MVC-Based Applications
4. Building a Simple JavaScript Frontend App with Constraint Validation
1. New Issues
2. Make a JavaScript Data Model
3. Set up the folder structure and create four initial files
3.1. Style the user interface with CSS
3.2. Provide general utility functions and JavaScript fixes in library files
3.3. Create a start page
4. Write the Model Code
4.1. Summary
4.2. Encode the model class as a constructor function
4.3. Encode the check operations
4.4. Encode the setter operations
4.5. Add a serialization function
4.6. Data management operations
5. The View and Controller Layers
5.1. The data management UI pages
5.2. Initialize the app
5.3. Initialize the data management use cases
5.4. Set up the user interface
6. Run the App and Get the Code
III. Associations
5. Associations
1. Reference Properties
2. Inverse Reference Properties
3. Reference Properties and Associations
4. Associations and Reference Properties
5. Making an Association-Free Information Design Model
5.1. The basic procedure
5.2. How to eliminate uni-directional associations
5.3. How to eliminate bi-directional associations
5.4. The resulting association-free design model
6. Building a Complete JavaScript Frontend App
1. Make a JavaScript Data Model
2. Write the Model Code
2.1. New issues
2.2. Summary
2.3. Encode each class of the JavaScript data model as a constructor function
2.4. Encode the property checks
2.5. Encode the setter operations
2.6. Encode the add and remove operations
2.7. Encode any other operation
2.8. Implement any inheritance relationship
2.9. Take care of deletion dependencies
2.10. Load all data on application startup
3. The View and Controller Layers
3.1. Initialize the app
3.2. Show information about associated objects in the List Objects use case
3.3. Allow selecting associated objects in the create and update use cases

List of Figures

2.1. The object type Book.
2.2. The minimal app's start page index.html.
3.1. The object type Person
3.2. The object type Person with an interval constraint
3.3. The object type Book with a pattern constraint
3.4. The object type Book with a uniqueness constraint
3.5. The object type Book with a standard identifier declaration
4.1. A platform-independent design model with the class Book and two invariants
4.2. Deriving a JavaScript data model from an information design model
4.3. The simple app's start page index.html.
5.1. A committee has a club member as chair expressed by a reference property
5.2. A committee has a club member as chair expressed by an association end with a "dot"
5.3. A model of the Committee-has-ClubMember-as-chair association without ownership dots
5.4. Models of the unidirectional association ClubMember has Committee as a chairedCommittee
5.5. Models of the bidirectional association between Committee and ClubMember
5.6. The Publisher-Book-Person information design model
5.7. Turn a non-functional target association end into a corresponding reference property with a pluralized form of the target class name
5.8. Turn a bi-directional one-to-one association into a pair of mutually inverse single-valued reference properties
5.9. Turn a bi-directional many-to-many association into a pair of mutually inverse multi-valued reference properties
5.10. The association-free design model
6.1. The complete JavaScript data model

List of Tables

2.1. Table 1: An associative array representing a collection of books
2.2. Table 2: A collection of book objects represented as a table
4.1. Datatype mapping
5.1. Different terminologies
5.2. Functionality types

Foreword

This four-part tutorial shows how to build frontend web applications with plain JavaScript, not using any (third-party) framework or library. A frontend web application 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.

Part I. Get Started

In this first part of our tutorial we summarize the web's foundations and show how to build a minimal effort frontend web application with plain JavaScript, not using any (third-party) framework or library.

The minimal version of a JavaScript frontend data management application discussed in this tutorial only includes a minimum 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 styling the user interface with CSS rules, and by adding further important parts of the app's overall functionality:

  1. Handling integrity constraints.

  2. Handling associations between object types.

  3. Handling subtype (inheritance) relationships between object types.

We discuss the handling of integrity constraints in Part II of our tutorial, whiere the considered simple app is still limited to a single object type ("books"). We discuss associations and inheritance in Parts III and IV, which deal with multiple object types ("books", "publishers" and "authors").

Chapter 1. Foundations

If you are already familiar with HTML, XML and JavaScript, you can skip this chapter and immediately start developing a minimal front-end web application with JavaScript in the following chapter.

1. The World Wide Web (WWW)

After the Internet had been established in the 1980'ies, Tim Berners-Lee developed the idea and the first infrastructure components of the WWW in 1989 at the European research institution CERN in Geneva, Switzerland. The WWW (or, simply, "the web") is based on

2. HTML and XML

HTML allows to mark up (or describe) the structure of a human-readable web document or web user interface, while XML allows to mark up the structure of all kinds of documents, data files and messages, whether they are human-readable or not. HTML can be based on XML.

2.1. XML documents

XML provides a syntax for expressing structured information in the form of an XML document with elements and their attributes. The specific elements and attributes used in an XML document can come from any vocabulary, such as public standards or your own user-defined XML format. XML is used for specifying

  • document formats, such as XHTML5, the Scalable Vector Graphics (SVG) format or the DocBook format,

  • data interchange file formats, such as the Mathematical Markup Language (MathML) or the Universal Business Language (UBL),

  • message formats, such as the web service message format SOAP

2.2. Unicode and UTF-8

XML is based on Unicode, which is a platform-independent character set that includes almost all characters from most of the world's script languages including Hindi, Burmese and Gaelic. Each character is assigned a unique integer code in the range between 0 and 1,114,111. For example, the Greek letter π has the code 960, so it can be inserted in an XML document as π (using the XML entity syntax).

Unicode includes legacy character sets like ASCII and ISO-8859-1 (Latin-1) as subsets.

The default encoding of an XML document is UTF-8, which uses only a single byte for ASCII characters, but three bytes for less common characters.

Almost all Unicode characters are legal in a well-formed XML document. Illegal characters are the control characters with code 0 through 31, except for the carriage return, line feed and tab. It is therefore dangerous to copy text from another (non-XML) text to an XML document (often, the form feed character creates a problem).

2.3. XML namespaces

Generally, namespaces help to avoid name conflicts. They allow to reuse the same (local) name in different namespace contexts.

XML namespaces are identified with the help of a namespace URI (such as the SVG namespace URI "http://www.w3.org/2000/svg"), which is associated with a namespace prefix (such as "svg"). Such a namespace represents a collection of names, both for elements and attributes, and allows namespace-qualified names of the form prefix:name (such as "svg:circle" as a namespace-qualified name for SVG circle elements).

A default namespace is declared in the start tag of an element in the following way:

<html xmlns="http://www.w3.org/1999/xhtml">

This example shows the start tag of the HTML root element, in which the XHTML namespace is declared as the default namespace.

The following example shows a namespace declaration for the SVG namespace:

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
   ...
  </head>
  <body>
    <figure>
      <figcaption>Figure 1: A blue circle</figcaption>
      <svg:svg xmlns:svg="http://www.w3.org/2000/svg">
        <svg:circle cx="100" cy="100" r="50" fill="blue"/>
      </svg:svg>
    </figure>
  </body>
</html>

2.4. Correct XML documents

XML defines two syntactic correctness criteria. An XML document must be well-formed, and if it is based on a grammar (or schema), then it must also be valid against that grammar.

An XML document is called well-formed, if it satisfies the following syntactic conditions:

  1. There must be exactly one root element.

  2. Each element has a start tag and an end tag; however, empty elements can be closed as <phone/> instead of <phone></phone>.

  3. Tags don't overlap, e.g. we cannot have

    <author><name>Lee Hong</author></name>
  4. Attribute names are unique within the scope of an element, e.g. the following code is not correct:

    <attachment file="lecture2.html" file="lecture3.html"/>

An XML document is called valid against a particular grammar (such as a DTD or an XML Schema), if

  1. it is well-formed,

  2. and it respects the grammar.

2.5. The evolution of HTML

The World-Wide Web Committee (W3C) has developed the following important versions of HTML:

  • HTML4 as an SGML-based language (in 1997),

  • XHTML 1 as an XML-based version of HTML4 (in 2000),

  • (X)HTML5 in competition and cooperation with the WHAT working group led by Ian Hickson and supported by browser vendors (in 2014).

HTML was originally designed as a structure description language, and not as a presentation description language. But HTML4 has a lot of purely presentational elements such as font. XHTML has been taking HTML back to its roots, dropping presentational elements and defining a simple and clear syntax, in support of the goals of

  • device independence,

  • accessibility, and

  • usability.

In the tutorial chapters of this book we only use XHTML5 because we prefer the clear syntax of XML documents over the liberal and confusing HTML4-style syntax of HTML5. The following simple example shows the basic code template to be used for any XHTML5 document:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 <head>
  <meta charset="UTF-8" />
  <title>XHTML5 Template Example</title>
 </head>
 <body>
  <h1>XHTML5 Template Example</h1>
  <section><h1>First Section Title</h1>
   ...
  </section>
 </body>
</html>

2.6. HTML forms

For user-interactive web applications, the web browser needs to render a user interface. The traditional metaphor for a software application's user interface is that of a form. The special elements for data input, data output and form actions are called form controls. An HTML form is a section of a document consisting of block elements that contain controls and labels on those controls.

Users complete a form by entering text into input fields and by selecting items from choice controls. A completed form is submitted with the help of a submit button. When a user submits a form, it is sent to a web server either with the HTTP GET method or with the HTTP POST method. The standard encoding for the submission is called URL-encoded. It is represented by the Internet media type application/x-www-form-urlencoded. In this encoding, spaces become plus signs, and any other reserved characters become encoded as a percent sign and hexadecimal digits, as defined in RFC 1738.

Each control has both an initial value and a current value, both of which are strings. The initial value is specified with the control element's value attribute, except for the initial value of a textarea element, which is given by its initial contents. The control's current value is first set to the initial value. Thereafter, the control's current value may be modified through user interaction or scripts. When a form is submitted for processing, some controls have their name paired with their current value and these pairs are submitted with the form.

Labels are associated with a control by including the control as a subelement of a label element ("implicit labels"), or by giving the control an id value and referencing this id in the for attribute of the label element ("explicit labels"). Notice that implicit labels are, in 2014, still not well supported by CSS libraries and assistive technologies. Therefore, explicit labels seem preferable, despite the fact that they imply quite some overhead and repetitive code.

In the simple user interfaces of our "Getting Started" applications, we only need two types of form controls: single line input fields created with <input name="..." /> and push buttons created with <button type="button">...</button>. An example of an HTML form with implicit labels for creating such a user interface is

<form id="Book">
  <p><label>ISBN: <input name="isbn" /></label></p>
  <p><label>Title: <input name="title" /></label></p>
  <p><label>Year: <input name="year" /></label></p>
  <p><button type="button" id="saveButton">Save</button></p>
</form>

3. JavaScript

This section provides a brief overview of JavaScript, assuming that the reader is already familiar with basic programming concepts and has some experience with programming, for instance, in PHP, Java or C#.

JavaScript is a programming language that can be used for

  1. Enriching a web page by

    • generating browser-specific HTML content or CSS styling,

    • inserting dynamic HTML content,

    • producing special audio-visual effects (animations).

  2. Enriching a web user interface by

    • implementing advanced user interface components,

    • validating user input on the client side,

    • automatically pre-filling certain form fields.

  3. Implementing a frontend web application with local data storage.

  4. Implementing a frontend component for a distributed web application with remote data storage managed by a backend component (server-side program).

3.1. Types and data literals in JavaScript

JavaScript has three primitive data types: string, number and boolean. There are three reference types: object, array and function. Arrays and functions are just special kinds of objects. Types are not declared and not checked since a JavaScript program is not compiled. Type conversion (casting) is performed automatically.

The value of a variable may be

  • a data value: either a string, a number, or a boolean,

  • an object: either an ordinary object, or an array, or a function,

  • one of the following two special data values: undefined or null.

All numeric data values are represented in 64-bit floating point format with an optional exponent (like in the numeric data literal 3.1e10). There is no type distinction between integers and floating point numbers. If an arithmetic expression cannot be evaluated to a number, its value is set to NaN ("not a number").

Logical (Boolean) data values are not only represented by the two pre-defined data literals true and false, but also by the empty string, the (numerical) data literal 0 as well as undefined and null are evaluated to false. Like in Java, the Boolean operator symbols are the exclamation mark ! for NOT, the double ampersand && for AND, and the double bar || for OR. For equality and inequality testing, we use the triple equality symbol === and !== instead of the double equality symbol == and !=. Otherwise, for instance, the number 2 would be the same as the string "2", since the condition (2 == "2") evaluates to true in JavaScript.

3.2. Different kinds of objects

A JavaScript object is essentially a set of name-value-pairs, also called slots. Objects can be created in an ad-hoc manner, using JavaScript's object literal notation, without instantiating a class:

var person1 = { lastName:"Smith", firstName:"Tom"};
var o1 = {};  // an empty object with no slots

Whenever the name in a slot is an admissible JavaScript identifier, the slot may be either a property slot or a function slot. Otherwise, if the name is some other type of string (in particular when it contains any blank space), then the slot represents an associative array element, as explained below.

The name in a property slot may denote either

  1. a data-valued property, in which case the value is a data value or, more generally, a data-valued expression;

    or

  2. an object-valued property, in which case the value is an object reference or, more generally, an object-valued expression.

The name in a function slot denotes a JavaScript function (better called "method"), and its value is a function definition text.

Object properties can be accessed in two ways:

  1. Using the dot notation (like in C++/Java):

    person1.lastName = "Smith"
  2. Using an associative array notation:

    person1["lastName"] = "Smith"

JavaScript objects can be used in many different ways for different purposes. Here are five different use cases for, or possible meanings of, JavaScript objects:

  1. A record is a set of property slots like, for instance,

    var myRecord = { firstName:"Tom", lastName:"Smith", age:26}
  2. An associative array (or 'hash map') supports look-ups of values based on keys like, for instance,

    var numeral2number = { "one":"1", "two":"2", "three":"3"}

    which associates the value "1" with the key "one", "2" with "two", etc.

  3. An untyped object does not instantiate a class. It may have property slots and function slots like, for instance,

    var person1 = { 
      lastName: "Smith", 
      firstName: "Tom",
      getInitials: function () {
        return this.firstName.charAt(0) + this.lastName.charAt(0); 
      }  
    };
  4. A namespace may be defined in the form of an untyped object referenced by a global object variable, the name of which represents a namespace prefix. For instance, the following object variable provides the main namespace of an application based on the Model-View-Controller (MVC) architecture paradigm where we have three subnamespaces corresponding to the three parts of an MVC application:

    var myApp = { model:{}, view:{}, ctrl:{} };
  5. A typed object instantiates a class defined by a JavaScript constructor function, as explained below.

3.3. Arrays

An array variable may be initialized with an array literal:

var a = [1,2,3];

JavaScript arrays can grow dynamically: it is possible to use indexes that are greater than the length of the array. For instance, after the array variable initialization above, the array held by the variable a has the length 3, but still we can assign a fifth array element like in

a[4] = 7;

3.4. Associative arrays

An associative array (also called 'hash map') provides a map from keys to their associated values. The keys of an associative array may be string literals that include blank spaces like in

var myTranslation = { 
    "my house": "mein Haus", 
    "my boat": "mein Boot", 
    "my horse": "mein Pferd"
}

An associative array is processed with the help of a special loop where we loop over all keys of the associative array using the pre-defined function Object.keys(a), which returns an array of all keys of an associative array a. For instance,

for (var i=0; i < Object.keys( myTranslation).length; i++) {
  key = Object.keys( myTranslation)[i]);
  alert('The translation of '+ key +' is '+ myTranslation[key]);
}

3.5. Defining and instantiating a class

A class can be defined in two steps. First define the constructor function that defines the properties of the class and assigns them the values of the constructor's parameters:

function Person( first, last) {
  this.firstName = first; 
  this.lastName = last; 
}

Next, define the instance-level methods of the class as function slots of the prototype object property of the constructor:

Person.prototype.getInitials = function () {
  return this.firstName.charAt(0) + this.lastName.charAt(0); 
}

Finally, class-level ("static") methods can be defined as function slots of the constructor, as in

Person.checkName = function (n) {
  ... 
}

An instance of a class is created by applying the new operator to the constructor:

var pers1 = new Person("Tom","Smith");

The method getInitials is invoked on the Person object pers1 by using the 'dot notation':

alert("The initials of the person are: " + pers1.getInitials());

3.6. Class hierarchies

For a subclass definition, we use the following 3-step code pattern. First we define the subclass constructor where we also invoke the superclass constructor:

function Student( first, last, matrNo) {
  Person.call( this, first, last);  // invoke superclass constructor
  this.matrNo = matrNo;  // define and assign additional properties
} 

By invoking the superclass constructor for the new object created as an instance of the subclass, we achieve that the properties created in the superclass constructor are also created for the subclass instance, along the entire chain of superclasses within a given class hierarchy. This mechanism takes care of property inheritance.

After defining the subclass constructor we assign a new empty superclass instance to its prototype property and reset the prototype's constructor property:

// making Student a subclass of Person
Student.prototype = new Person();
// reset the subtype's constructor
Student.prototype.constructor = Student;

By assigning an empty superclass instance to the prototype property of the subclass constructor, we achieve that the methods defined in, and inherited by, the superclass are also available for objects instantiating the subclass. This mechanism of chaining the prototypes takes care of method inheritance.

Finally, we define the additional methods of the subclass:

Student.prototype.setMatrNo = function (matrNo) {
  this.matrNo = matrNo; 
}

3.7. Enumeration datatypes

We implement an enumeration datatype in the form of a special object definition:

GenderEL = Object.defineProperties( {}, {
    MALE: { value: 1, writable: false},
    FEMALE: { value: 2, writable: false},
    MAX: { value: 2, writable: false}
});

This definition of the special object GenderEL allows using the enumeration literals GenderEL.MALE and GenderEL.FEMALE standing for the integer values 1 and 2. We can then check if an enumeration attribute (such as an attribute gender of type GenderEL) has an admissible value by testing if its value is not smaller than 1 and not greater than the MAX property of the enumeration object (GenderEL.MAX in our example).

3.8. JavaScript as an object-oriented language

JavaScript is object-oriented, but in a different way than classical object-oriented programming languages such as Java and C++. There is no explicit class concept, but a more or less equivalent concept of a constructor. A constructor definition allows grouping properties and methods that apply to objects created by the constructor. Objects can be created without instantiating a class. Methods can be defined for specific objects independently of any class. An inheritance mechanism is provided via the constructor's built-in prototype property. At run time, properties and methods can be added to, or removed from, any object and class.

Glossary

Hypertext Transfer Protocol

HTTP is a stateless request/response protocol using human-readable text messages for the communication between web clients and web servers. The main purpose of HTTP has been to allow fetching web documents identified by URLs from a web browser, and invoking the operations of a backend web application program from a HTML form executed by a web browser. More recently, HTTP is increasingly used for providing web APIs and web services.

Hypertext Markup Language

HTML allows marking up (or describing) the structure of a human-readable web document or web user interface. The XML-based version of HTML, which is called "XHTML5", provides a simpler and cleaner syntax compared to traditional HTML.

Extensible Markup Language

XML allows to mark up the structure of all kinds of documents, data files and messages, whether they are human-readable or not. XML is based on Unicode. SVG and MathML are based on XML, and there is an XML-based version of HTML.

XML provides a syntax for expressing structured information in the form of an XML document with elements and their attributes. The specific elements and attributes used in an XML document can come from any vocabulary, such as public standards or user-defined XML formats.

Chapter 2. Building a Minimal JavaScript Frontend App in Seven Steps

In this chapter, we build a minimal frontend web application with JavaScript. The purpose of our example app is to manage information about books. That is, we deal with a single object type: Book, as depicted in Figure 2.1.

Figure 2.1. The object type Book.

The object type Book.

What do we need for such an information management application? There are four standard use cases, which have to be supported by the application:

  1. Create: Enter the data of a book that is to be added to the collection of managed books.

  2. Read: Show a list of all books in the collection of managed books.

  3. Update the data of a book.

  4. Delete a book record.

For entering data with the help of the keyboard and the screen of our computer, we can use HTML forms, which provide the user interface technology for web applications.

For maintaining a collection of data objects, we need a storage technology that allows to keep data objects in persistent records on a secondary storage device, such as a harddisk or a solid state disk. Modern web browsers provide two such technologies: the simpler one is called Local Storage, and the more powerful one is called IndexDB. For our minimal example app, we use Local Storage.

1. Step 1 - Set up the Folder Structure

In the first step, we set up our folder structure for the application. We pick a name for our app, such as "Public Library", and a corresponding (possibly abbreviated) name for the application folder, such as "publicLibrary". Then we create this folder on our computer's disk and a subfolder "src" for our JavaScript source code files. In this folder, we create the subfolders "model", "view" and "ctrl", following the Model-View-Controller paradigm for software application architectures. And finally we create an index.html file for the app's start page, as discussed below. Thus, we end up with the following folder structure:

publicLibrary
  src
    ctrl
    model
    view
  index.html

The start page of the app loads the Book.js model class file and provides a menu for choosing one of the CRUD data management operations performed by a corresponding page such as, for instance, createBook.html, or for creating test data with the help of the procedure Book.createTestData() in line 17, or clearing all data with Book.clearData() in line 18:

Figure 2.2. The minimal 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>Minimal JS Frontend App Example</title>
  <script src="src/model/Book.js"></script>
</head>
<body>
  <h1>Public Library</h1>
  <h2>An Example of a Minimal JavaScript Frontend 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>

2. Step 2 - Write the Model Code

In the second step, we write the code of our model class in a specific JavaScript file. In the information design model shown in Figure 2.1 above, there is only one class, representing the object type Book. So, in the folder src/model, we create a file Book.js that initially contains the following code:

function Book( slots) {
  this.isbn = slots.isbn;
  this.title = slots.title;
  this.year = slots.year;
};

The model class Book is encoded as a JavaScript constructor function with a single slots parameter, which is supposed to be a record object with properties isbn, title and year, representing values for the ISBN, the title and the year attributes of the class Book. Therefore, in the constructor function, the values of the slots properties are assigned to the coresponding attributes whenever a new object is created as an instance of this class.

In addition to defining the model class in the form of a constructor function, we also define the following items in the Book.js file:

  1. A class-level property Book.instances representing the collection of all Book instances managed by the application in the form of an associative array.

  2. A class-level method Book.loadAllInstances for loading all managed Book instances from the persistent data store.

  3. A class-level method Book.saveAllInstances for saving all managed Book instances to the persistent data store.

  4. A class-level method Book.createRow for creating a new Book instance.

  5. A class-level method Book.updateRow for updating an existing Book instance.

  6. A class-level method Book.deleteRow for deleting a Book instance.

  7. A class-level method Book.createTestData for creating a few example book records to be used as test data.

  8. A class-level method Book.clearData for clearing the book datastore.

2.1. Representing the collection of all Book instances

For representing the collection of all Book instances managed by the application, we define and initialize the class-level property Book.instances in the following way:

Book.instances = {};

So, initially our collection of books is empty. In fact, it's defined as an empty object, since we want to represent it in the form of an associative array (a set of key-value slots, also called 'hashmap') where an ISBN is a key for accessing the corresponding book object (as the value associated with the key). We can visualize the structure of such an associative array in the form of a lookup table, as shown in Table 1.

Table 2.1. Table 1: An associative array representing a collection of books

Key Value
006251587X { isbn:"006251587X," title:"Weaving the Web", year:2000 }
0465026567 { isbn:"0465026567," title:"Gödel, Escher, Bach", year:1999 }
0465030793 { isbn:"0465030793," title:"I Am A Strange Loop", year:2008 }

Notice that the values of this associative array are simple objects corresponding to table rows. Consequently, we could represent them also in a simple table, as shown in Table 2.

Table 2.2. Table 2: A collection of book objects represented as a table

ISBN Title Year
006251587X Weaving the Web 2000
0465026567 Gödel, Escher, Bach 1999
0465030793 I Am A Strange Loop 2008

2.2. Loading all Book instances

For persistent data storage, we use Local Storage, which is a HTML5 JavaScript API supported by modern web browsers. Loading the book records from Local Storage involves three steps:

  1. Retrieving the book table that has been stored as a large string with the key "bookTable" from Local Storage with the help of the assignment

    bookTableString = localStorage["bookTable"];

    This retrieval is performed in line 5 of the program listing below.

  2. Converting the book table string into a corresponding associative array bookTable with book rows as elements, with the help of the built-in procedure JSON.parse:

    bookTable = JSON.parse( bookTableString);

    This conversion, performed in line 11 of the program listing below, is called deserialization.

  3. Converting each row of bookTable (representing an untyped record object) into a corresponding object of type Book stored as an element of the associative array Book.instances, with the help of the procedure convertRow2Obj defined as a "static" (class-level) method in the Book class:

    Book.convertRow2Obj = function (bookRow) {
      var book = new Book( bookRow);
      return book;
    };

Here is the full code of the procedure:

Book.loadAllInstances = function () {
  var key="", keys=[], bookTableString="", bookTable={};  
  try {
    if (localStorage["bookTable"]) {
      bookTableString = localStorage["bookTable"];
    }
  } catch (e) {
    alert("Error when reading from Local Storage\n" + e);
  }
  if (bookTableString) {
    bookTable = JSON.parse( bookTableString);
    keys = Object.keys( bookTable);
    console.log( keys.length +" books loaded.");
    for (var i=0; i < keys.length; i++) {
      key = keys[i];
      Book.instances[key] = Book.convertRow2Obj( bookTable[key]);
    }
  }
};

Notice that since an input operation like localStorage["bookTable"] may fail, we perform it in a try-catch block, where we can follow up with an error message whenever the input operation fails.

2.3. Saving all Book instances

Saving all book objects from the Book.instances collection in main memory to Local Storage in secondary memory involves two steps:

  1. Converting the associative array Book.instances into a string with the help of the predefined JavaScript procedure JSON.stringify:

    bookTableString = JSON.stringify( Book.instances);

    This conversion is called serialization.

  2. Writing the resulting string as the value of the key "bookTable" to Local Storage:

    localStorage["bookTable"] = bookTableString;

These two steps are performed in line 5 and in line 6 of the following program listing:

Book.saveAllInstances = function () {
  var bookTableString="", error=false,
      nmrOfBooks = Object.keys( Book.instances).length;  
  try {
    bookTableString = JSON.stringify( Book.instances);
    localStorage["bookTable"] = bookTableString;
  } catch (e) {
    alert("Error when writing to Local Storage\n" + e);
    error = true;
  }
  if (!error) console.log( nmrOfBooks + " books saved.");
};

2.4. Creating a new Book instance

The Book.createRow procedure takes care of creating a new Book instance and adding it to the Book.instances collection :

Book.createRow = function (slots) {
  var book = new Book( slots);
  Book.instances[slots.isbn] = book;
  console.log("Book " + slots.isbn + " created!");
};

2.5. Updating an existing Book instance

For updating an existing Book instance we first retrieve it from Book.instances, and then re-assign those attributes the value of which has changed:

Book.updateRow = function (slots) {
  var book = Book.instances[slots.isbn];
  var year = parseInt( slots.year);
  if (book.title !== slots.title) { book.title = slots.title;}
  if (book.year !== year) { book.year = year;}
  console.log("Book " + slots.isbn + " modified!");
};

Notice that in the case of a numeric attribute (such as year), we have to make sure that the value of the corresponding input parameter (y), which is typically obtained from user input via an HTML form, is converted from String to Number with one of the two type conversion functions parseInt and parseFloat.

2.6. Deleting an existing Book instance

A Book instance is deleted from the Book.instances collection by first testing if the associative array has an element with the given key (line 2), and then applying the JavaScript built-in delete operator:, which deletes a slot from an object, or, in our case, an element from an associative array:

Book.deleteRow = function (isbn) {
  if (Book.instances[isbn]) {
    console.log("Book " + isbn + " deleted");
    delete Book.instances[isbn];
  } else {
    console.log("There is no book with ISBN " + isbn + " in the database!");
  }
};

2.7. Creating test data

For being able to test our code, we may create some test data and save it in our Local Storage database. We can use the following procedure for this:

Book.createTestData = function () {
  Book.instances["006251587X"] = new Book({isbn:"006251587X", title:"Weaving the Web", year:2000});
  Book.instances["0465026567"] = new Book({isbn:"0465026567", title:"Gödel, Escher, Bach", year:1999});
  Book.instances["0465030793"] = new Book({isbn:"0465030793", title:"I Am A Strange Loop", year:2008});
  Book.saveAllInstances();
};

2.8. Clearing all data

The following procedure clears all data from Local Storage:

Book.clearData = function () {
  if (confirm("Do you really want to delete all book data?")) {
    localStorage["bookTable"] = "{}";
  }
};

3. Step 3 - Initialize the Application

We initialize the application by defining its namespace and MVC subnamespaces. Namespaces are an important concept in software engineering and many programming languages, including Java and PHP, provide specific support for namespaces, which help grouping related pieces of code and avoiding name conflicts. Since there is no specific support for namespaces in JavaScript, we use special objects for this purpose (we may call them "namespace objects"). First we define a root namespace (object) for our app, and then we define three subnamespaces, one for each of the three parts of the application code: model, view and controller. In the case of our example app, we may use the following code for this:

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

Here, the main namespace is defined to be pl, standing for "Public Library", with the three subnamespaces model, view and ctrl being initially empty objects. We put this code in a separate file initialize.js in the ctrl folder, because such a namespace definition belongs to the controller part of the application code.

4. Step 4 - Implement the List Objects Use Case

This use case corresponds to the "Read" from the four basic data management use cases Create-Read-Update-Delete (CRUD).

The user interface for this use case is provided by the following HTML page containing an HTML table for displaying the book objects. For our example app, this page would be called listBooks.html (in the main folder publicLibrary) and would contain the following HTML code:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Simple JS Frontend App Example</title>
  <script src="src/ctrl/initialize.js"></script>
  <script src="src/model/Book.js"></script>
  <script src="src/view/listBooks.js"></script>
  <script>
   window.addEventListener( "load", pl.view.listBooks.setupUserInterface);
  </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>

Notice that this HTML file loads three JavaScript files: the controller file src/ctrl/initialize.js, the model file src/model/Book.js and the view file src/view/listBooks.js. The first two files contain the code for initializing the app and for the model class Book as explained above, and the third one, which represents the UI code of the "list books" operation, is developed now. In fact, for this operation, we just need a procedure for setting up the data management context and the UI, called setupUserInterface:

 pl.view.listBooks = {
  setupUserInterface: function () {
    var tableBodyEl = document.querySelector("table#books>tbody");
    var keys=[], key="", row={};
    // load all book objects
    Book.loadAllInstances();
    keys = Object.keys( Book.instances);
    // for each book, create a table row with three cells for the three attributes
    for (var i=0; i < keys.length; i++) {
      key = keys[i];
      row = tableBodyEl.insertRow();
      row.insertCell(-1).textContent = Book.instances[key].isbn;      
      row.insertCell(-1).textContent = Book.instances[key].title;  
      row.insertCell(-1).textContent = Book.instances[key].year;
    }
  }
};

The simple logic of this procedure consists of two steps:

  1. Read the collection of all objects from the persistent data store (in line 6).

  2. Display each object as a row in a HTML table on the screen (in the loop starting in line 9).

More specifically, the procedure setupUserInterface first creates the book objects from the corresponding rows retrieved from Local Storage by invoking Book.loadAllInstances() and then creates the view table in a loop over all key-value slots of the associative array Book.instances where each value represents a book object. In each step of this loop, a new row is created in the table body element with the help of the JavaScript DOM operation insertRow(), and then three cells are created in this row with the help of the DOM operation insertCell(): the first one for the isbn property value of the book object, and the second and third ones for its title and year property values. Both insertRow and insertCell have to be invoked with the argument -1 for making sure that new elements are appended to the list of rows and cells.

5. Step 5 - Implement the Create Object Use Case

For a data management operation with user input, such as the "create object" operation, an HTML page with an HTML form is required as a user interface. The form has a form field for each attribute of the Book class. For our example app, this page would be called createBook.html (in the app folder publicLibrary) and would contain the following HTML code:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Minimal JS Frontend App Example</title>    
  <script src="src/ctrl/initialize.js"></script>
  <script src="src/model/Book.js"></script>
  <script src="src/view/createBook.js"></script>
  <script>
   window.addEventListener("load", pl.view.createBook.setupUserInterface);
  </script>
</head>
<body>
  <h1>Public Library: Create a new book record</h1>
  <form id="Book">
    <p><label>ISBN: <input name="isbn" /></label></p>
    <p><label>Title: <input name="title" /></label></p>
    <p><label>Year: <input name="year" /></label></p>
    <p><button type="button" id="saveButton">Save</button></p>
  </form>
  <nav><a href="index.html">Back to main menu</a></nav>
</body>
</html>

The view code file src/view/createBook.js contains two procedures:

  1. setupUserInterface takes care of retrieveing the collection of all objects from the persistent data store and setting up an event handler (handleSaveButtonClickEvent) on the save button for handling click button events by saving the user input data;

  2. handleSaveButtonClickEvent reads the user input data from the form fields and then saves this data by calling the Book.saveRow procedure.

pl.view.createBook = {
  setupUserInterface: function () {
    var saveButton = document.getElementById("saveButton");
    // load all book objects
    Book.loadAllInstances();
    // Set an event handler for the save/submit button
    saveButton.addEventListener("click", 
        pl.view.createBook.handleSaveButtonClickEvent);
  },
  handleSaveButtonClickEvent: function () {
    var frmEl = document.forms['Book'];
    var isbn = frmEl.isbn.value;
    var title = frmEl.title.value;
    var year = frmEl.year.value;
    Book.saveRow( isbn, title, year);
    frmEl.reset();
  }
};

6. Step 6 - Implement the Upate Object Use Case

Again, we have an HTML page for the user interface (updateBook.html) and a view code file (src/view/updateBook.js). The form for the UI of the "update object" operation has a selection field for choosing the book to be updated, and a form field for each attribute of the Book class. However, the form field for the standard identifier attribute (ISBN) is read-only because we do not allow changing the standard identifier of an existing object.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Minimal JS Frontend App Example</title>
  <script src="src/ctrl/initialize.js"></script>
  <script src="src/model/Book.js"></script>
  <script src="src/view/updateBook.js"></script>
  <script>
   window.addEventListener("load", pl.view.updateBook.setupUserInterface);
  </script>
</head>
<body>
  <h1>Public Library: Update a book record</h1>
  <form id="Book" action="">
    <p>
      <label>Select book: 
        <select name="selectBook"><option value=""> --- </option></select>
      </label>
    </p>
    <p><label>ISBN: <input name="isbn" disabled="disabled" /></label></p>
    <p><label>Title: <input name="title" /></label></p>
    <p><label>Year: <input name="year" /></label></p>
    <p><button type="button" id="updateButton">Save Changes</button></p>
  </form>
  <nav><a href="index.html">Back to main menu</a></nav>
</body>
</html>

The setupUserInterface procedure now has to set up a selection field by retrieveing the collection of all book objects from the persistent data store for populating the select element's option list:

pl.view.updateBook = {
  setupUserInterface: function () {
    var selectEl = document.forms['Book'].selectBook;
    var isbnInpEl = document.forms['Book'].isbn;
    var titleInpEl = document.forms['Book'].title;
    var yearInpEl = document.forms['Book'].year;
    var updateButton = document.getElementById("updateButton");
    var bookKey="", book = null, bookOptionEl = null;
    // load all book objects
    Book.loadAllInstances();
    // populate the selection list with books
    for (bookKey in Book.instances) {
      book = Book.instances[ bookKey];
      bookOptionEl = document.createElement("option");
      bookOptionEl.text = book.title;
      bookOptionEl.value = book.isbn;
      selectEl.add( bookOptionEl, null);
    }
    // when a book is selected, populate the form with the data of the selected book
    selectEl.addEventListener("change", function () {
      bookKey = selectEl.value;
      if (bookKey) {
        book = Book.instances[bookKey];
        isbnInpEl.value = book.isbn;
        titleInpEl.value = book.title;
        yearInpEl.value = book.year;
      } else {
        isbnInpEl.value = "";
        titleInpEl.value = "";
        yearInpEl.value = "";
      }
    });
    updateButton.addEventListener("click", 
        pl.view.updateBook.handleUpdateButtonClickEvent);
  },
  handleUpdateButtonClickEvent: function () {
    var frmEl = document.forms['Book'];
    var isbn = frmEl.isbn.value;
    var title = frmEl.title.value;
    var year = frmEl.year.value;
    Book.saveRow( isbn, title, year);
    frmEl.reset();
  }
};

7. Step 7 - Implement the Delete Object Use Case

For the "delete object" use case, the UI form just has a selection field for choosing the book to be deleted:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Minimal JS Frontend App Example</title>
  <script src="src/ctrl/initialize.js"></script>
  <script src="src/model/Book.js"></script>
  <script src="src/view/deleteBook.js"></script>
  <script>
   window.addEventListener("load", pl.view.deleteBook.setupUserInterface);
  </script>
</head>
<body>
  <h1>Public Library: Delete a book record</h1>
  <form id="Book">
    <p>
      <label>Select book: 
        <select name="selectBook"><option value=""> --- </option></select>
      </label>
    </p>
    <p><button type="button" id="deleteButton">Delete</button></p>
  </form>
  <nav><a href="index.html">Back to main menu</a></nav>
</body>
</html>

The view code in src/view/deleteBook.js consists of the following two procedures:

pl.view.deleteBook = {
  setupUserInterface: function () {
    var deleteButton = document.getElementById("deleteButton");
    var selectEl = document.forms['Book'].selectBook;
    var isbn="", book=null, bookOptionEl = null;
    // load all book objects
    Book.loadAllInstances();
    // populate the selection list with books
    for (isbn in Book.instances) {
      book = Book.instances[ isbn];
      bookOptionEl = document.createElement("option");
      bookOptionEl.text = book.title;
      bookOptionEl.value = book.isbn;
      selectEl.add( bookOptionEl, null);
    }
    deleteButton.addEventListener("click", 
        pl.view.deleteBook.handleDeleteButtonClickEvent);
  },
  handleDeleteButtonClickEvent: function () {
    var selectEl = document.forms['Book'].selectBook;
    var isbn = selectEl.value;
    if (isbn) {
      Book.deleteRow( isbn);
      selectEl.remove( selectEl.selectedIndex);
    }
  }
};

8. Run the App and Get the Code

You can run the minimal app from our server or download it as a ZIP archive file.

Part II. Integrity Constraints

Chapter 3. Integrity Constraints and Data Validation

For detecting non-admissible and inconsistent data and for preventing such data to be added to an application's database, we need to define suitable integrity constraints that can be used by the application's data validation mechanisms for catching these cases of flawed data. Integrity constraints are logical conditions that must be satisfied by the data in the model objects stored in the application's database. For instance, if an application is managing data about persons including their birth dates and their death dates, if they have already died, then we must make sure that for any person object with a death date, this date is not before that person object's birth date.

Integrity constraints may take many different forms. For instance, property constraints define conditions on the admissible property values of an object of a certain type. We concentrate on the most important kinds of property constraints:

Mandatory Value Constraints

require that a property must have a value. For instance, a person must have a name, so the name attribute must not be empty.

Range Constraints

require that a property must have a value from the value space of the type that has been defined as its range. For instance, an integer attribute must not have the value "aaa".

Interval Constraints

require that an attribute's value must be in a specific interval.

Pattern Constraints

require that a string attribute's value must satisfy a certain pattern, typically defined by a regular expression.

Uniqueness Constraints

require that a property's value is unique among all instances of the given object type.

Referential Integrity Constraints

require that the values of a reference property refer to an existing object in the range of the reference property

The visual language of UML class diagrams supports defining integrity constraints with the help of invariants expressed either in plain English or in the Object Constraint Language (OCL) and shown in a special type of rectangle attached to the model element concerned. We use UML class diagrams for making design models that are independent of a specific programming language or technology platform.

UML class diagrams provide special support for expressing multiplicity (or cardinality) constraints. This type of constraint allows to specify a lower multiplicity ( minimum cardinality) or an upper multiplicity (maximum cardinality), or both, for a property or an association end. In UML, this takes the form of a multiplicity expression l..u where the lower multiplicity l is either a non-negative integer or the special value *, standing for unbounded, and the upper multiplicity u is either a positive integer not smaller than l or the special value *. For showing property multiplicity constrains in a class diagram, multiplicity expressions are enclosed in brackets and appended to the property name in class rectangles, as shown in the Person class rectangle below in Figure 3.1.

Since integrity maintenance is fundamental in database management, the data definition language part of the relational database language SQL supports the definition of integrity constraints in various forms. On the other hand, however, integrity constraints and data validation are not supported at all in common programming languages such as PHP, Java, C# or JavaScript. It is therefore important to choose an application development framework that provides sufficient support for them Notice that in HTML5, there is some support of data validation for user input in form fields.

In the following sections we discuss the different types of property constraints listed above in more detail. We also show how to express them in UML class diagrams and in SQL.

1. Mandatory Value Constraints

A mandatory value constraint requires that a property must have a value. This can be expressed in a UML class diagram with the help of a multiplicity constraint expression where the lower multiplicity is 1. For a single-valued property, this would result in the multiplicity expression 1..1, or the simplified expression 1, appended to the property name in brackets, as shown in Figure 3.1 below.

Figure 3.1. The object type Person

The object type Person

Whenever a class rectangle does not show a multiplicity expression for a property, the property is mandatory (and single-valued), that is, the multiplicity expression 1 is the default for properties.

In an SQL table creation statement, a mandatory value constraint is expressed in a table column definition by appending the key phrase NOT NULL to the column definition as in the following example:

CREATE TABLE persons(
  name  VARCHAR(30) NOT NULL,
  age   INTEGER
)

According to this table definition, any row of the persons table must have a value in the column name, but not necessarily in the column age.

2. Range Constraints

A range constraint requires that a property must have a value from the value space of the type that has been defined as its range. This is implicitly expressed by defining a type for a property as its range. For instance, the attribute age defined for the object type Person in Figure 3.1 above has the range Integer, so it must not have a value like "aaa", which does not denote an integer. However, it may have values like -13 or 321, which also do not make sense as the age of a person. In a similar way, since its range is String, the attribute name may have the value "" (the empty string), which is a valid string that does not make sense as a name.

We can avoid allowing negative integers like -13 as age values, and the empty string as a name, by assigning more specific datatypes to these attributes, such as NonNegativeInteger to age, and NonEmptyString to name. Notice that such more specific type definitions are neither predefined in SQL nor in common programming languages, so we have to implement them either by user-defined types, as supported in SQL-99 database management systems such as PostgreSQL, or by using suitable additional constraints such as interval constraints, which are discussed in the next section.

3. Interval Constraints

An interval constraint requires that an attribute's value must be in a specific interval, which is specified by a minimum value or a maximum value, or both. Such a constraint can be defined for any attribute having an ordered type, but normally we define them only for numeric datatypes or calendar datatypes. For instance, we may add an interval constraint requiring that the age attribute value must be in the interval [0,120] to the Person class rectangle as in Figure 3.2 below.

Figure 3.2. The object type Person with an interval constraint

The object type Person with an interval constraint

In an SQL table creation statement, an interval constraint is expressed in a table column definition by appending a suitable CHECK clause to the column definition as in the following example:

CREATE TABLE persons(
  name  VARCHAR(30) NOT NULL,
  age   INTEGER CHECK (age >= 0 AND age <= 120)
)

4. Pattern Constraints

A pattern constraint requires that a string attribute's value must match a certain pattern, typically defined by a regular expression. For instance, for the object type Book we define an isbn attribute with the datatype String as its range and add a pattern constraint requiring that the isbn attribute value must be a 10-digit string or a 9-digit string followed by "X" to the Book class rectangle shown in Figure 3.3 below.

Figure 3.3. The object type Book with a pattern constraint

The object type Book with a pattern constraint

In an SQL table creation statement, a pattern constraint is expressed in a table column definition by appending a suitable CHECK clause to the column definition as in the following example:

CREATE TABLE books(
  isbn   VARCHAR(10) NOT NULL CHECK (isbn ~ '^\d{9}(\d|X)$'),
  title  VARCHAR(50) NOT NULL
)

The ~ (tilde) symbol denotes the regular expression matching predicate and the regular expression ^\d{9}(\d|X)$ follows the syntax of the POSIX standard (see, e.g. the PostgreSQL documentation).

5. Uniqueness Constraints

A uniqueness constraint requires that a property's value is unique among all instances of the given object type. For instance, for the object type Book we can define the isbn attribute to be unique in a UML class diagram by appending the keyword unique in curly braces to the attribute's definition in the Book class rectangle shown in Figure 3.4 below.

Figure 3.4. The object type Book with a uniqueness constraint

The object type Book with a uniqueness constraint

In an SQL table creation statement, a uniqueness constraint is expressed by appending the keyword UNIQUE to the column definition as in the following example:

CREATE TABLE books(
  isbn   VARCHAR(10) NOT NULL UNIQUE,
  title  VARCHAR(50) NOT NULL
)

6. Standard Identifiers (Primary Keys)

An attribute (or, more generally, a combination of attributes) can be declared to be the standard identifier for objects of a given type, if it is mandatory and unique. We can indicate this in a UML class diagram with the help of a (user-defined) stereotype «stdid» assigned to the attribute isbn as shown in Figure 3.5 below.

Figure 3.5. The object type Book with a standard identifier declaration

The object type Book with a standard identifier declaration

Notice that a standard identifier declaration implies both a mandatory value and a uniqueness constraint on the attribute concerned.

Standard identifiers are called primary keys in relational databases. We can declare an attribute to be the primary key in an SQL table creation statement by appending the phrase PRIMARY KEY clause to the column definition as in the following example:

CREATE TABLE books(
  isbn   VARCHAR(10) PRIMARY KEY,
  title  VARCHAR(50) NOT NULL
)

7. Referential Integrity Constraints

A referential integrity constraint requires that the values of a reference property refer to an existing object in the range of the reference property. Since we do not deal with reference properties in this part of the tutorial, we postpone the discussion of referential integrity constraints to the next part of our tutorial.

8. Constraints and Validation in MVC-Based Applications

Integrity constraints should be defined in the model classes of an MVC-based application since they are part of the semantics of a model class (representing a business object type). However, a more difficult question is where to perform data validation? In the model? Or in the view? Or in both?

We have seen that a relational database management system (DBMS) performs data validation whenever there is an attempt to change certain data in the persistent database, provided that all relevant integrity constraints have beend defined in the form of SQL constraints. This is indeed essential since we want to avoid, under all circumstances, that invalid data enters the persistent database. However, it requires that we somehow duplicate the integrity constraint code, because we want to encapsulate it in the code of the model class to which the constraint belongs. The common approach of automatically mapping model classes to corresponding SQL table definitions with the help of an object-relational mapping framework (such as Hibernate) does typically not consider the mapping of constraints, despite the fact that they are an esential part of the application's data semantics.

If the DBMS would be the only application component that validates the data, this would create a latency, and hence usability, problem in distributed applications because the user would not get immediate feedback on invalid input data. This problem is well-known from classical web applications where the frontend component submits the user input data via HTML form submission to a backend component running on a remote web server. Only this backend component validates the data and returns the validation results in the form of a set of error messages to the front end, Only then, typically several seconds later, and in the hard-to-digest form of a bulk message does the user get the validation feedback. This approach is no longer considered acceptable today. Rather, the user should get immediate validation feedback on each single input data item.

So, we need a validation mechanism in the user interface of an application. Fortunately, HTML5 supports this need. But it is not sufficient to perform data validation in the user interface. We also need to do it in the model for making sure that no flawed data enters the application's persistent data store. This creates the problem of how to maintain the constraints in one place (the model), but use them in two places (in the model and in the view)-

Unfortunately, many application development frameworks for MVC-based applications provide only little or even no support at all for integrity constraints and data validation.

Chapter 4. Building a Simple JavaScript Frontend App with Constraint Validation

The minimal JavaScript frontend app that we have discussed in the first part of this three part tutorial has been limited to support the minimum functionality of a data management app only. However, it did not take care of preventing the users 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 user interface built with HTML5.

We still consider the simple single-class data management problem that was considered in the minimal app tutorial. So, again, the purpose of our simple app is to manage information about books. But now we also consider the data integrity rules (also called 'business rules') that govern the management of book data. These integrity rules, or constraints, can be expressed in a UML class diagram as as shown in Figure 4.1 below.

Figure 4.1. A platform-independent design model with the class Book and two invariants

A platform-independent design model with the class Book and two invariants

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().

In addition to these constraint, there are the implicit range constraints defined by assigning the datatypes NonEmptyString 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.

1. New Issues

Compared to the minimal app discussed in the first part of the tutorial we have to deal with a number of new issues:

  1. In the model code we have to take care of adding for every property

    1. a check function for validating the constraints defined for the property,

    2. a setter method that invokes the check function and is to be used for setting the value of the property.

  2. In the user interface ("view") code we have to take care of

    1. immediate constraint validation on user input,

    2. styling the user interface with CSS rules.

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 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 in the create and update data management methods of the model class.

2. Make a JavaScript Data Model

Using the information design model shown in Figure 4.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 4.2. Deriving a JavaScript data model from an information design model

Deriving a JavaScript data model from an information design model
Deriving a JavaScript data model from an information design model
Deriving a JavaScript data model from an information design model

The JavaScript data model defines the model class Book with attributes and methods, where the names of class-level ("static") methods are underlined.

3. 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.

3.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.

3.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.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 Section 3.2,

  2. initialize.js from the src/ctrl folder, defining the app's MVC namespaces, as discussed in the minimal app tutorial in Section 3,

  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 discussed in Section 4.

Figure 4.3. The simple 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>Simple JS Frontend 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>An Example of a Simple JavaScript Frontend 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>

4. Write the Model Code

How to Encode a JavaScript Data Model

The JavaScript data model shown on the right hand side in Figure 4.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.

4.1. Summary

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

  2. Encode the check operations, 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 set 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. If the property is coupled to an inverse reference propertiy (implementing a bi-navigable association), make sure that the setter operation also assigns/adds/removes corresponding references to/from (the value set of) the inverse property.

  4. Encode any other operation.

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

4.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 properties of a slots object parameter.

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 type 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.

Table 4.1. Datatype mapping

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 = {};

4.3. Encode the check operations

Encode the check operations 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 in Section 3.3 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 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.4. Encode the setter operations

Encode the setters 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;
  }
};

4.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 characteristics of it. By convention, these functions are called toString(). In the case of the Book class, we add the following code:

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

4.6. Data management operations

In addition to defining the model class in the form of a constructor function with property definitions, check and set operations, 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 the first part of our tutorial, 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. 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 + " !");      
    }
  }
};

5. 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 in Section 3.3.

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.

5.1. The data management UI pages

Each data management use case 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 (see Section 5.3).

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>Simple JS Frontend 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>Simple JS Frontend 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="button" id="saveButton">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.

5.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.

5.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.

5.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 attaching event handlers to form controls, in the case of "list books" we only have to render a table displaying all the books, as in the following view/listBooks.js code:

pl.view.listBooks = {
  setupUserInterface: function () {
    var tableBodyEl = document.querySelector("table#books>tbody");
    var keys = Object.keys( Book.instances);
    var key="", row={};
    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 handleSaveButtonClickEvent() in line 7 of the following program listing (and discussed below), 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, 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) in line 17 of the following view/createBook.js code:

pl.view.createBook = {
  setupUserInterface: function () {
    var saveButton = document.getElementById("saveButton");
    var isbnInpEl = document.forms['Book'].isbn;
    var titleInpEl = document.forms['Book'].title;
    var yearInpEl = document.forms['Book'].year;
    saveButton.addEventListener("click", this.handleSaveButtonClickEvent);
    isbnInpEl.addEventListener("input", function () { 
      isbnInpEl.setCustomValidity( Book.checkIsbnAsId( isbnInpEl.value).message);
    });
    titleInpEl.addEventListener("input", function () { 
      titleInpEl.setCustomValidity( Book.checkTitle( titleInpEl.value).message);
    });
    yearInpEl.addEventListener("input", function () { 
      yearInpEl.setCustomValidity( Book.checkYear( yearInpEl.value).message);
    });
    window.addEventListener("beforeunload", function () {
      Book.saveAllInstances(); 
    });
  },
  handleSaveButtonClickEvent: function () {
    ...
  }
};

Notice that, like in line 8 of the program listig above, the form data validation functions are added as listeners for input events, which are fired during 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 returning an error message in the case of a constraint violation. So, whenever the string represented by the expression Book.checkIsbn(isbnInpEl.value).message is empty, everything is fine. Otherwise, if it represents an error message, the browser indicates the constraint violation to the user by marking the form field concerned (typicallly with a red border) and by displaying the error message.

In the following handleSaveButtonClickEvent() function, the property check functions are set for invoking the predefined checkValidity() function on the form in line 9 below. The provided data is only saved (by invoking createRow in line 10 below), if this does not detect any constraint violation.

handleSaveButtonClickEvent: 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 provide 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);
    formEl.reset();
  }
}

The logic of the setupUserInterface methods for the update and delete use cases is similar.

6. Run the App and Get the Code

You can run the minimal app from our server or download it as a ZIP archive file.

Part III. Associations

Chapter 5. Associations

An association between object types classifies relationships between objects of those types. For instance, the association Person-isEmployedBy-Enterprise may classify the relationships PeterMiller-isEmployedBy-IBM, SusanSmith-isEmployedBy-IBM and SarahAnderson-isEmployedBy-Google between the objects PeterMiller, SusanSmith and SarahAnderson of type Person as well as Google and IBM of type Enterprise. In other words, associations are relationship types with two or more object types participating in them. An association between two object types is called binary. While binary associations are more common, we may also have to deal with n-ary associations, where n is a natural number greater than 2. For instance, Person-isTreatedIn-Hospital-for-Disease is a 3-ary ("ternary") association between the object types Person, Hospital and Disease. However, in this tutorial we only discuss binary associations. For simplicity, we just say 'association' when we actually mean 'binary association'.

While individual relationships (such as PeterMiller-isEmployedBy-IBM) are important information items, associations are important elements of information models. Consequently, software applications have to implement them in a proper way, typically as part of their model layer within a model-view-controller (MVC) architecure. Unfortunately, application development frameworks do often not provide much support for dealing with associations.

In mathematics, associations have been formalized in an abstract way as sets of uniform tuples, called relations. In Entity-Relationship (ER) modeling, which is the classical information modeling approach in information systems and software engineering, objects are called entities, and associations are called relationship types. The Unified Modeling Language (UML) includes the UML Class Diagram language for information modeling. In UML, object types are called classes, relationship types are called associations, and individual relationships are called "links". These three terminologies are summarized in the following table:

Table 5.1. Different terminologies

Our preferred term(s) UML ER Diagrams Mathematics
object object entity individual
object type, class class entity type unary relation
relationship link relationship tuple
association, relationship type association relationship type relation


We first discuss reference properties, which represent a special form of binary associations.

1. Reference Properties

A property is defined for an object type, or class, which is its domain. The values of a property are either data values from some datatype, in which case the property is called an attribute, or they are object references referencing an object from some class, in which case the property is called a reference property. For instance, the class Committee shown in Figure 5.1 below has an attribute name with range String, and a reference property chair with range ClubMember.

Figure 5.1. A committee has a club member as chair expressed by a reference property

A committee has a club member as chair expressed by a reference property

Object-oriented programming languages, such as JavaScript, PHP and Java, directly support the concept of reference properties, which are properties whose range is not a datatype but a reference type, or class, and whose values are object references to instances of that class.

By default, the multiplicity of a property is 1, which means that the property is mandatory and functional (or single-valued), having exactly one value, like the property chair in class Committee shown in Figure 5.1. When a functional property is optional (not mandatory), it has the multiplicity 0..1, which means that the property's minimum cardinality is 0 and its maximum cardinality is 1.

2. Inverse Reference Properties

For being able to easily retrieve the committees that are chaired or co-chaired by a clubmember, we add two more reference properties to our model: the property of a club member to be the chair of a committee and the property of a club member to be the co-chair of a committee. We assume that any club member may be the chair or co-chair of at most one committee (where the disjunction is non-exclusive). So, we get the following model:

Notice that there is a close correspondence between the two reference properties Committee::chair and ClubMember::chairedCommittee. They are the inverse of each other: when the club member Tom is the chair of the budget committee, expressed by the tuple ("budget committee", "Tom"), then the budget committee is the committee chaired by the club member Tom, expressed by the inverse tuple ("Tom", "budget committee"). For expressing this inverse correspondence in the diagram, we append an inverse property constraint, inverse of chair, in curly braces to the declaration of the property ClubMember::chairedCommittee, and a similar one to the property Committee::chair, as shown in the following diagram:

We also call Committee::chair and ClubMember::chairedCommittee a pair of mutually inverse reference properties. Having such a pair in a model means having redundancy because each of the two involved reference properties can be derived from the other by inversion. Redundancy means data storage overhead and update overhead. This is the price to pay for the bidirectional navigabilty, which supports efficient retrieval, obtained through the inverse reference property pair.

For maintaining the duplicate information of an inverse reference property pair, it is good practice to treat one of the two involved properties as the master, and the other one as the slave, and take this distinction into consideration in the code of the change methods (such as the property setters) of the affected model class. We indicate the slave of an inverse reference property pair in a model diagram by declaring the slave property to be a derived property using the UML notation of a slash as a prefix of the property name as shown in the following diagram:

The property chairedCommittee in ClubMember now has a /-prefix indicating that it is derived.

In a UML class diagram, the derivation of a property can be specified, for instance, by an OCL expression that evaluates to the value of the property for the given object. In the case of a property being the inverse of another property, specified by the constraint expression {inverse of anotherProperty} appended to the property declaration, the derivation expression is implied. In our example, it evaluates to the committee object reference c such that c.chair = this.

There are two ways how to realize the derivation of a property: it may be derived on read via a read-time computation of its value, or it may be derived on update via an update-time computation performed whenever one of the variables in the derivation expression (typically, another property) changes its value. The latter case corresponds to a materialzed view in an SQL database. While a reference property that is derived on read may not guarantee efficient navigation, because the on-read computation may create unacceptable latencies, a reference property that is derived on update does provide effificient navigation.

In the case of a derived reference property, the derivation expresses life cycle dependencies. These dependencies require special consideration in the code of the affected model classes by providing a number of change management mechanisms based on the functionality of the represented association (either one-to-one, many-to-one or many-to-many). The following table provides an overview about the different cases of functionality of an association:

Table 5.2. Functionality types

Functionality type Meaning
one-to-one both functional and inverse functional
many-to-one functional
one-to-many inverse functional
many-to-many neither functional nor inverse functional


In our example of the derived inverse reference property ClubMember::chairedCommittee, which is functional (single-valued) and optional, this means that

  1. whenever a new committee object is created (with a mandatory chair assignment), the corresponding ClubMember::chairedCommittee property is assigned;

  2. whenever the chair property is updated (that is, a new chair is assigned to a committee), the corresponding ClubMember::chairedCommittee property is updated as well;

  3. whenever a committee object is destroyed, the corresponding ClubMember::chairedCommittee property is unassigned.

In the case of a derived inverse reference property that is non-functional (multi-valued) while its inverse base property is functional (like Publisher::publishedBooks in Figure 5.10 below being derived from Book::publisher), the life cycle dependencies imply that

  1. whenever a new 'base object' (such as a book) is created, the corresponding inverse property is updated by adding a reference to the new base object to its value set (like adding a reference to the new book object to Publisher::publishedBooks );

  2. whenever the base property is updated (e.g., a new publisher is assigned to a book), the corresponding inverse property (in our example, Publisher::publishedBooks) is updated as well by removing the old object reference from its value set and adding the new one;

  3. whenever a base object (such as a book) is destroyed, the corresponding inverse property is updated by removing the reference to the base object from its value set (like removing a reference to the book object to be destroyed from Publisher::publishedBooks ).

We use the /-pefix notation in the example above for indicating that the property ClubMember::chairedCommittee is derived on update from the corresponding committee object.

3. Reference Properties and Associations

A reference property (such as chair in the example shown in Figure 5.1 above) can be visualized in a UML class diagram in the form of an association end. This requires to connect the domain class and the range class of the reference property with an association line and annotate the end of this line at the range class side with a "dot", with the property name and with a multiplicity symbol, as shown in Figure 5.2 below for the case of our example. Thus, the two diagrams shown in Figure 5.1 and Figure 5.2 express essentially equivalent models.

Figure 5.2. A committee has a club member as chair expressed by an association end with a "dot"

A committee has a club member as chair expressed by an association end with a "dot"

When a reference property is represented by an association end with a "dot", like chair in Figure 5.1, then the property's multiplicity is attached to the association end. Since in a complete model, all association ends need to have a multiplicity, we also have to define a multiplicity for the other end at the side of the Committee class, which represents the inverse of the property. This multiplicity (of the inverse property) is not available in the original property description in the model shown in Figure 5.1, so it has to be added according to the intended semantics of the association. It can be obtained by answering the question "is it mandatory that an instance of ClubMember is the chair of an instance of Committee?" for finding the minimum cardinality and the question "can an instance of ClubMember be the chair of more than one Committee?" for finding the maximum cardinality.

When the value of a property is a set of values from its range, the property is non-functional and its multiplicity is either 0..* or n..* where n > 0. Instead of 0..*, which means "neither mandatory nor functional", one can simply write the asterisk symbol *.

4. Associations and Reference Properties

According to the UML specification, an association end can be "owned" by the class at the other end. This means that the owned association end corresponds to a reference property of the class at the other end. The fact that an association end is owned by the class at the other end is visually expressed with the help of a small filled circle (also called a "dot") at the end of the association line. This is illustrated in Figure 5.2 above, where the "dot" at the association end chair indicates that the class Committee has a reference property chair with range ClubMember.

When we make an information model in the form of a UML class diagram, we typically end up with a model containing one or more associations that do not have any ownership defined for their ends, as, for instance, in Figure 5.3 below. When there is no ownership dot at either end of an association, such as in this example, this means that the model does not specify how the association is to be represented (or implemented) with the help of reference properties. Such an association does not have any direction. According to the UML 2.5 specification, the ends of such an association are "owned" by itself, and not by any of the classes participating in it.

Figure 5.3. A model of the Committee-has-ClubMember-as-chair association without ownership dots

A model of the Committee-has-ClubMember-as-chair association without ownership dots

A model without association end ownership dots is acceptable as a relational database design model, but it is incomplete as an information design model for classical object-oriented (OO) programming languages. For instance, the model of Figure 5.3 provides a relational database design with two entity tables, committees and clubmembers, and a separate one-to-one relationship table committee_has_clubmember_as_chair. But it does not provide a design for Java classes, since it does not specify how the association is to be implemented with the help of reference properties.

There are three options how to turn a model without association end ownership dots into a complete OO design model where all associations are either uni-directional or bi-directional: we can place an ownership dot at either end or at both ends of the association. Each of these three options defines a different way how to represent, or implement, the association with the help of reference properties. So, for the association shown in Figure 5.3 above, we have the following options:

  1. Place an ownership dot at the chair association end, leading to the model shown in Figure 5.2 above, which can be turned into the association-free model shown in Figure 5.1 above.

  2. Place an ownership dot at the chairedCommittee association end, leadig to the completed models shown in Figure 5.4 below.

  3. Make the association bi-directional by placing ownership dots at both association ends with the meaning that the association is implemented in a redundant manner by a pair of mutually inverse reference properties Committee::chair and ClubMember::chairedCommittee, as shown in Figure 5.5.

Figure 5.4. Models of the unidirectional association ClubMember has Committee as a chairedCommittee

Models of the unidirectional association ClubMember has Committee as a chairedCommittee
Models of the unidirectional association ClubMember has Committee as a chairedCommittee

So, whenever we have modeled an association, we have to make a choice, which of its ends represents a reference property and will therefore be marked with an ownership dot. It can be either one, or both. This decision also implies a decision about the navigability of the association. When an association end represents a reference property, this implies that it is navigable (via this property).

Figure 5.5. Models of the bidirectional association between Committee and ClubMember

Models of the bidirectional association between Committee and ClubMember
Models of the bidirectional association between Committee and ClubMember

The model shown in Figure 5.6 below (about publishers, books and their authors) serves as our running example in all other parts of the tutorial. Notice that it contains two bidirectional associations, as indicated by the ownership dots at both association ends.

Figure 5.6. The Publisher-Book-Person information design model

The Publisher-Book-Person information design model

5. Making an Association-Free Information Design Model

How to eliminate associations from a design model by replacing them with reference properties

Since classical OO programming languages do not support assocations as first class citizens, but only classes and reference properties representing implicit associations, we have to eliminate all explicit associations for obtaining an OO design model.

5.1. The basic procedure

The starting point of our association elimination procedure is an information design model with various kinds of uni-directional and bi-directional associations, such as the model shown in Figure 5.6 above. If the model still contains any non-directional associations, we first have to turn them into directional ones by making a decision on the ownership of their ends, which is typically based on navigability requirements.

Notice that both associations in the Publisher-Book-Person information design model, publisher-publishedBooks and authoredBooks-authors (or Authorship), are bi-directional as indicated by the ownership dots at both association ends. For eliminating all explicit associations from an information design model, we have to perform the following steps:

  1. Eliminate uni-directional associations, connecting a source with a target class, by replacing them with a reference property in the source class such that the target class is its range.

  2. Eliminate bi-directional associations by replacing them with a pair of mutually inverse reference properties.

5.2. How to eliminate uni-directional associations

A uni-directional association connecting a source with a target class is replaced with a corresponding reference property in its source class having the target class as its range. Its multiplicity is the same as the multiplicity of the target association end. Its name is the name of the association end, if there is any, otherwise it is set to the name of the target class (possibly pluralized, if the reference property is multi-valued).

This replacement procedure is illustrated for the case of a functional uni-directional association in Figure 5.4 above. For the case of a non-functional uni-directional association, Figure 5.7 below provides an illustration.

Figure 5.7. Turn a non-functional target association end into a corresponding reference property with a pluralized form of the target class name


5.3. How to eliminate bi-directional associations

A bi-directional association, such as Authorship in the model shown in Figure 5.6 above, is replaced with a pair of mutually inverse reference properties, such as Book::authors and Person::authoredBooks. Since both reference properties represent the same information (the same set of binary relationships), it's an option to conisder one of them being the master and the other one the slave, which is derived from the master. We discuss the two cases of a one-to-one and a many-to-many association

  1. In the case of a bi-directional one-to-one association, this leads to a pair of mutually inverse single-valued reference properties, one in each of the two associated classes. Since both of them represent essentially the same information (one of them is the inverse of the other), one has to choose which of them is considered the "master", and which of them is the "slave", where the "slave" property is considered to represent the inverse of the "master". In the slave class, the reference property representing the inverse association is designated as a derived property that is automatically updated whenever 1) a new master object is created, 2) the master reference property is updated, or 3) a master object is destroyed.

    Figure 5.8. Turn a bi-directional one-to-one association into a pair of mutually inverse single-valued reference properties


  2. A bi-directional many-to-many association is mapped to a pair of mutually inverse multi-valued reference properties, one in each of the two classes participating in the association. Again, in one of the two classes, the multi-valued reference property representing the (inverse) association is designated as a derived property that is automatically updated whenever the corresponding property in the other class (where the association is maintained) is updated.

    Figure 5.9. Turn a bi-directional many-to-many association into a pair of mutually inverse multi-valued reference properties


5.4. The resulting association-free design model

After replacing all associations of the design model with reference properties, we obtain the following association-free model:

Figure 5.10. The association-free design model

The association-free design model

After the platform-independent association-free information design model has been completed, one or more platform-specific data models, for a choice of specific implementation platforms, can be derived from it. Such a platform-specific data model can still be expressed in the form of a UML class diagram, but it contains only modeling elements that can be directly encoded in the chosen platform. Thus, for any platform considered, the tutorial contains two sections: 1) how to make the platform-specific data model, and 2) how to encode this model.

Chapter 6. Building a Complete JavaScript Frontend App

The two apps that we have discussed in previous chapters, the minimal app and the simple app, have been limited to managing the data of one object type only. A real app, however, has to manage the data of several object types, which are typically related to each other in various ways. In particular, there may be associations and generalization (inheritance) relationships between object types. Handling associations and generalization are advanced issues in software application engineering, which are often not well supported by application development frameworks. We adopt the approach of model-based development, which provides a general methodology for engineering data management apps.

For being able to understand this tutorial, you need to understand the underlying concepts and theory. Either you first read the theory chapters on associations and generalization, before you read this tutorial, or you start reading this tutorial and consult the theory chapters whenever you stumble upon a term that you don't know.

In this tutorial, we show

  1. how to derive a JavaScript data model from an information design model,

  2. how to encode the JavaScript data model in the form of JavaScript model classes,

  3. how to write the view and controller code based on the model code.

1. Make a JavaScript Data Model

The starting point for making a JavaScript data model is an association-free information design model like the following one:

How to make this design model has been discussed in the previous section (about associations). We now show how to derive a JavaScript data model from this design model.

  1. Create a check operation for each non-derived property in order to have a central place for implementing property constraints. For a standard identifier property (such as Book::isbn), three check operations are needed:

    1. A basic check operationm, such as checkIsbn, for checking all syntactic constraints, but not the mandatory value and the uniqueness constraints.

    2. A check operation, such as checkIsbnAsId, for checking the mandatory value and uniqueness constraints that are required for an identifier (or primary key) attribute.

    3. A check operation, such as checkIsbnAsIdRef, for checking the referential integrity constraint that is required for an ID reference (IdRef) (or foreign key) attribute.

    For a reference property, such as Book::publisher, the check operation, Book.checkPublisher, has to check the corresponding referential integrity constraint, and possibly also a mandatory value constraint, if the property is mandatory.

  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.

This leads to the following JavaScript data model class Book, where the class-level ('static') methods are shown underlined:

We have to perform a similar transformation also for the classes Publisher and Person. This gives us the complete JavaScript data model derived from the above association-free model, as depicted in the following class diagram.

Figure 6.1.  The complete JavaScript data model


2. Write the Model Code

How to Encode a JavaScript Data Model

The JavaScript data model can be directly encoded for getting the code of the model layer of our JavaScript frontend app.

2.1. New issues

Compared to the simple app discussed in a previous tutorial, we have to deal with a number of new technical issues:

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

    1. Reference properties for which we need to deal with referential integrity constraints (ID references) and which need special care in the serialization and de-serialization procedures.

    2. Deletion dependencies: whenever the semantics of an association implies an existential dependency of objects of type B on objects of type A, this requires that when an object of type A is deleted, all dependent objects of type B have to be deleted as well.

    3. Inverse reference property maintenance: for any reference property that has an inverse property, whenever its value is changed, this requires a corresponding change of the inverse property's value.

  2. In the UI code we have to take care of

    1. Showing information about associated objects in the list objects use case.

    2. Allowing to select associated objects from a list of all existing objects in the create object and update object use cases.

2.2. Summary

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

  2. Encode the property checks in the form of class-level ('static') methods. Take care that all constraints of a property as specified in the JavaScript data model are properly encoded in the property checks.

  3. Encode the property setters as (instance-level) methods. In each setter, the corresponding property check is invoked and the property is only set, if the check does not detect any constraint violation. If the property is the inverse of another reference property (representing a bi-directional association), make sure that the setter also assigns (or adds) corresponding references to (the value set of) the inverse property.

  4. Encode the add/remove operations as (instance-level) methods that invoke the corresponding property checks. If the multi-valued reference property is the inverse of another reference propertiy (representing a bi-directional association), make sure that both the add and the remove operation also assign/add/remove corresponding references to/from (the value set of) the inverse property.

  5. Encode any other operation.

  6. Take care of deletion dependencies in deleteRow methods.

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

2.3. Encode each class of the JavaScript data model as a constructor function

Each class C of the data model is encoded by means of a corresponding JavaScript constructor function with the same name C such that all non-derived properties are parameters whose type should be indicated in a comment. This requires to map the platform-independent data type names (such as String, Integer, Decimal, Date and Boolean) to the corresponding names of the implicit JavaScript data types[1] (in particular, this requires to map Integer and Decimal to Number.).

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, that is, without arguments. If the constructor is invoked with arguments, these values may be overwritten by calling the setter methods for all properties. It is good practice to put the optional parameters of the constructor functions at the end of the parameter list, so as to allow invoking the constructor without providing values for these optional parameters.

For instance, the Publisher class from the JavaScript data model is encoded in the following way:

function Publisher( n, a) {
  // set the default values for the parameter-free default constructor
  this.name = "";     // String
  this.address = "";  // String
  
  // derived reference properties
  this.publishedBooks = {};  // associative array of Book objects, inverse of Book::publisher

  // constructor invocation with arguments
  if (arguments.length > 0) {
    this.setName( n); 
    this.setAddress( a); 
  }
};

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

2.4. Encode the property checks

in the form of 'static' (class-level) methods. Take care that all constraints of the property as specified in the JavaScript data model are properly encoded in the check functions. 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, a constraint violation exception is thrown, using one of the exception classes MandatoryValueConstraintViolation, TypeConstraintViolation, ValueRangeConstraintViolation, PatternConstraintViolation, UniquenessConstraintViolation, ReferentialIntegrityConstraintViolation or OtherConstraintViolation, which are defined in the model code file model/constraintViolationTypes.js.

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

Publisher.checkName = function (n) {
  if (!n) {
    return new NoConstraintViolation();  
  } else if (typeof(n) !== "string" || n.trim() === "") {
    return new TypeConstraintViolation("The name must be a non-empty string!");
  } else {
    return new NoConstraintViolation();
  }
};

Notice that, since the name attribute is the standard ID attribute of Publisher, we only check syntactic constraint in checkName, and check the mandatory value and uniqueness constraints in checkNameAsId, which invokes checkName:

Publisher.checkNameAsId = function (n) {
  var constraintViolation = Publisher.checkName( n);
  if ((constraintViolation instanceof NoConstraintViolation)) {
    if (!n) {
      return new MandatoryValueConstraintViolation("A value for the name must be provided!");
    } else if (Publisher.instances[n]) {  
      constraintViolation = new UniquenessConstraintViolation('There is already a publisher record with this name!');
    } else {
      constraintViolation = new NoConstraintViolation();
    } 
  }
  return constraintViolation;
};

Since for any standard ID attribute, we may have to deal with ID references (foreign keys) in other classes, we need to provide a further check method, such as checkNameAsIdRef, for checking the referential integrity constraint, as illustrated in the following example:

Publisher.checkNameAsIdRef = function (n) {
  var constraintViolation = Publisher.checkName( n);
  if ((constraintViolation instanceof NoConstraintViolation)) {
    if (!Publisher.instances[n]) {  
      constraintViolation = new ReferentialIntegrityConstraintViolation('There is no publisher record with this name!');
    } 
  }
  return constraintViolation;
};

2.5. Encode the setter operations

Encode the setters 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. If the property is coupled to an inverse reference property (implementing a bi-directional association), make sure that the setter operation also assigns/adds/removes corresponding references to/from (the value set of) the inverse property.

For instance, the setter operation setName is encoded in the following way:

Publisher.prototype.setName = function (n) {
  var constraintViolation = Publisher.checkNameAsId( n);
  if (constraintViolation instanceof NoConstraintViolation) {
    this.name = n;
  } else {
    throw constraintViolation;
  }
};

An example of a setter for a reference property that is coupled to a derived inverse multi-valued reference property is the operation setPublisher:

Book.prototype.setPublisher = function (p) {
  var constraintViolation = null;
  var publisherIdRef = "";
  // a publisher can be given as ...
  if (typeof(p) !== "object") {  // an ID reference or 
    publisherIdRef = p;
  } else {                       // an object reference
    publisherIdRef = p.name;
  }
  constraintViolation = Book.checkPublisher( publisherIdRef);
  if (constraintViolation instanceof NoConstraintViolation) {
    if (this.publisher) {  // update existing book record
      // delete the obsolete inverse reference in Publisher::publishedBooks
      delete this.publisher.publishedBooks[ this.isbn];  
    }
    // create the new publisher reference 
    this.publisher = Publisher.instances[ publisherIdRef];
    // create the new inverse reference in Publisher::publishedBooks
    this.publisher.publishedBooks[ this.isbn] = this;  
  } else {
    throw constraintViolation;
  }
};

In this setter method, the Book.checkPublisher method is invoked, which checks the referential integrity constraint for the given publisher reference:

Book.checkPublisher = function (publisherIdRef) {
  var constraintViolation = null;
  if (!publisherIdRef) {
    constraintViolation = new MandatoryValueConstraintViolation("A publisher must be provided!");
  } else {
    constraintViolation = Publisher.checkNameAsIdRef( publisherIdRef);
  }
  return constraintViolation;
};

2.6. Encode the add and remove operations

Encode the add/remove operations as (instance-level) methods that invoke the corresponding check-as-IdRef operations. If the multi-valued reference property is coupled to an inverse reference propertiy, make sure that both the add and the remove operation also assigns/adds/removes corresponding references to/from (the value set of) the inverse property.

The add operation Book::addAuthor, which takes care of the maintenance of the multi-valued reference property Book::authors implementing the bi-directional authorship association between Book and Person, is encoded in the following way:

Book.prototype.addAuthor = function( a) {
  var constraintViolation = null;
  var authorIdRef = "";
  // an author can be given as ...
  if (typeof(a) !== "object") {  // an ID reference or
    authorIdRef = a;
  } else {                       // an object reference
    authorIdRef = a.personID;
  }
  constraintViolation = Book.checkAuthor( authorIdRef);
  if (constraintViolation instanceof NoConstraintViolation) {
    // add the new author reference
    this.authors[ authorIdRef] = Person.instances[ authorIdRef];
    // automatically update the inverse reference property Person::authoredBooks
    this.authors[ authorIdRef].authoredBooks[ this.isbn] = this;
  }
};

For the remove operation removeAuthor we obtain the following code:

Book.prototype.removeAuthor = function( a) {
  var constraintViolation = null;
  var authorIdRef = "";
  // an author can be given as an ID reference or an object reference
  if (typeof(a) !== "object") {
    authorIdRef = a;
  } else {
    authorIdRef = a.personID;
  }
  constraintViolation = Book.checkAuthor( authorIdRef);
  if (constraintViolation instanceof NoConstraintViolation) {
    // automatically update the inverse reference property Person::authoredBooks
    delete this.authors[ authorIdRef].authoredBooks[ this.isbn];
    // delete the author reference
    delete this.authors[ authorIdRef];
  }
}

An author reference is removed from the value set of the multi-valued reference property authors with the help of the JavaScript delete operation which deletes elements (or members) from an associative array (or object).

2.7. Encode any other operation

of the form operation( in param1, ...) of the class C as an instance-level method with the help of the JavaScript prototype mechanism in the form of C.prototype.operation = function (param1, ...) {...}.

2.8. Implement any inheritance relationship

When a class (say TextBook) inherits from, or specializes, another class (say Book), this is encoded by means of the following JavaScript code pattern:

function Book( isbn, title) {
  this.isbn = isbn; 
  this.title = title; 
}
function TextBook( isbn, title, subjectArea) {
  Book.call( this, isbn, title);          // invoke superclass constructor
  this.subjectArea = subjectArea;              // define and assign additional properties
}
TextBook.prototype = new Book();            // making Student a subtype of Person
TextBook.prototype.constructor = TextBook;  // adjust the subtype's constructor

2.9. Take care of deletion dependencies

Whenever the semantics of an association implies an existential dependency of objects of type B on objects of type A, this requires that when an object of type A is deleted, all dependent objects of type B have to be deleted as well. Consequently, we have to add the required code for handling these deletion dependencies in deleteRow methods, as shown in the following example:

Publisher.deleteRow = function (n) {
  var publisher = Publisher.instances[n];
  var key="", keys=[];
  console.log("Publisher " + Publisher.instances[n].name + " deleted.");
  // delete all dependent book records using JS member/property deletion
  keys = Object.keys( publisher.publishedBooks);
  for (var i=0; i < keys.length; i++) {
    key = keys[i];
    delete Book.instances[key];
  }
  // delete the publisher record
  delete Publisher.instances[n];
};

2.10. Load all data on application startup

For resource efficiency, the redundant information of inverse associations (such as publishedBooks) is not stored in the persistent data store. Therefore, it has to be reconstructed whenever the app's data is retrieved rom the persistent data store at app startup time.

Book.loadAllInstances = function () {
  var key="", keys=[], bookTable={}, book=null;  
  try {
    if(!localStorage["bookTable"]) {
      localStorage.setItem("bookTable", JSON.stringify({}));
    } else {
      bookTable = JSON.parse( localStorage["bookTable"]);
      console.log( Object.keys( bookTable).length +" books loaded.");
    }
  } catch (e) {
    alert("Error when reading from Local Storage\n" + e);        
  }
  keys = Object.keys( bookTable);
  for (var i=0; i < keys.length; i++) {
    key = keys[i];  // ISBN
    book = Book.convertRow2Obj( bookTable[key]);
    Book.instances[key] = book;
    // create the derived inverse reference in Publisher::publishedBooks
    book.publisher.publishedBooks[ key] = book;
  }
};

3. The View and Controller Layers

The user interface (UI) consist of a start page for navigating to the data management UI pages, one for each object type (in our example, books.html, publishers.html and persons.html). Each of these data management UI pages contains 5 sections, such as manage books, list books, create book, update book and delete book, such that only one of them is displayed at any time (by setting the CSS property display:none for all others).

3.1. Initialize the app

For initializing the data management use cases, the required data has to be loaded from persistent storage. This is performed in a controller procedure such as bl.ctrl.books.manage.initialize in ctrl/books.js with the following code:

bl.ctrl.books.manage = {
  initialize: function () {
    Publisher.loadAllInstances();
    Person.loadAllInstances();
    Book.loadAllInstances();
    bl.view.books.manage.setUpUserInterface();
  }
};

The initialize method for managing book data loads the publisher and the book tables since the book data management UI needs to provide selection list for all three object types.

3.2. Show information about associated objects in the List Objects use case

For showing information about published books in the list publishers use case, a corresponding column in the HTML table has to be created:

    var tableBodyEl = document.querySelector("div#listPublishers>table>tbody");
    var pKeys = Object.keys( Publisher.instances);
    var row=null, publisher=null, bKeys=[], listEl=null, listItemEl=null;
    tableBodyEl.innerHTML = "";
    for (var i=0; i < pKeys.length; i++) {
      publisher = Publisher.instances[pKeys[i]];
      row = tableBodyEl.insertRow(-1);
      row.insertCell(-1).textContent = publisher.name;      
      row.insertCell(-1).textContent = publisher.address;
      // create list of books published by this publisher
      listEl = document.createElement("ul");
      bKeys = Object.keys( publisher.publishedBooks);
      for (var j=0; j < bKeys.length; j++) {
        listItemEl = document.createElement("li");
        listItemEl.textContent = publisher.publishedBooks[bKeys[j]].title;
        listEl.appendChild( listItemEl);
      }
      row.insertCell(-1).appendChild( listEl);
    }

3.3. Allow selecting associated objects in the create and update use cases

For allowing to select objects to be associated with the current object from a list in the update book and delete object use cases, an HTML selection list is populated with the instances of the associated object type

    var publisherSelEl = bl.view.app.createPublisherSelectionListEl();
    var bookSelEl = bl.view.app.createBookSelectionListEl();
    // set up the publisher selection list
    updBookSelectPublisherLabelEl.innerHTML = "Publisher: ";
    updBookSelectPublisherLabelEl.appendChild( publisherSelEl);
    // set up the book selection list
    updBookSelectBookLabelEl.innerHTML = "Select book: ";
    updBookSelectBookLabelEl.appendChild( bookSelEl);
		

The selection list is created in the bl.view.app.createPublisherSelectionListEl() method:

  createPublisherSelectionListEl: function () {
    var publisherSelEl = document.createElement("select");
    var key="", keys=[], publisher={};
    var optionEl = document.createElement("option");
    optionEl.text = "";
    optionEl.value = "";
    publisherSelEl.add( optionEl, null);
    keys = Object.keys( Publisher.instances);
    for (var i=0; i < keys.length; i++) {
      key = keys[i];
      publisher = Publisher.instances[key];
      publisher.index = i+1;  // store selection list index
      optionEl = document.createElement("option");
      optionEl.text = publisher.name;
      optionEl.value = publisher.name;
      publisherSelEl.add( optionEl, null);
    }
    publisherSelEl.name = "publisher";
    return publisherSelEl;
  },		


[1] JavaScript has the following implicit primitive data types: Number, String, and Boolean. In addition, it has the reference type Object, specialized by Function and Array, and by pre-defined subtypes of Object such as Date.