Understanding and Implementing Information Management Concepts and Techniques

JavaScript Front-End Web App Tutorial Part 4: Managing Unidirectional Associations

Learn how to manage unidirectional associations between object types, such as the associations assigning publishers and authors to books

Gerd Wagner

Warning: This tutorial manuscript 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. You may run the example app from our server, or download it as a ZIP archive file. See also our Web Engineering project page.

This tutorial article, along with any associated source code, is licensed under The Code Project Open License (CPOL), implying that the associated code is provided "as-is", can be modified to create derivative works, can be redistributed, and can be used in commercial applications, but the article must not be distributed or republished without the author's consent.

2017-07-25

Revision History
Revision 0.520151112gw
Replace the <<stdid>> stereotype with a {id} property modifier in class diagrams; add practice project.
Revision 0.420150226gw
Replace "Club" with "Committee" in the example in the theory section
Revision 0.320150217gw
drop parts of the validation code, add section "Points of Attention"
Revision 0.220140512gw
add chapter about non-functional associations
Revision 0.120140401gw
create first version

Table of Contents

Foreword
1. Reference Properties and Unidirectional Associations
1. References and Reference Properties
2. Referential Integrity
3. Modeling Reference Properties as Unidirectional Associations
4. Representing Unidirectional Associations as Reference Properties
5. Adding Directionality to a Non-Directed Association
6. Our Running Example
7. Eliminating Unidirectional Associations
7.1. The basic elimination procedure restricted to unidirectional associations
7.2. Eliminating associations from the design model
8. Rendering Reference Properties in the User Interface
2. Implementing Unidirectional Functional Associations with Plain JavaScript
1. Implementing Single-Valued Reference Properties with Plain JavaScript
2. Make a JavaScript Class Model
3. New Issues
4. Write the Model Code
4.1. Summary
4.2. Code each model class as an ES6 class
4.3. Code the property checks
4.4. Code the property setters
4.5. Choose and implement a deletion policy
4.6. Serialization and Object-to-Record Mapping
5. Write the View and Controller Code
5.1. Initialize the app
5.2. Show associated objects in the Retrieve/List All use case
5.3. Selecting associated objects in the Create and Update use cases
3. Implementing Unidirectional Non-Functional Associations with Plain JavaScript
1. Implementing Multi-Valued Reference Properties in JavaScript
2. Make a JavaScript Class Model
3. New issues
4. Write the Model Code
4.1. Code the add and remove operations
4.2. Choose and implement a deletion policy
4.3. Serialization and Object-to-Record Mapping
5. Write the View Code
5.1. Show information about associated objects in the Retrieve/List All use case
5.2. Selecting associated objects in the Create use case
5.3. Selecting associated objects in the Update use case
6. How to Run the App, Get the Code and Ask Questions
7. Points of Attention
8. Practice Project

List of Figures

1.1. A committee has a club member as chair expressed by the reference property chair
1.2. An association end with a "dot"
1.3. Representing the unidirectional association ClubMember has Committee as chairedCommittee as a reference property
1.4. A model of a non-directed association between Committee and ClubMember
1.5. Modeling a bidirectional association between Committee and ClubMember
1.6. The Publisher-Book information design model with a unidirectional association
1.7. The Publisher-Book-Author information design model with two unidirectional associations
1.8. Turn a non-functional association end into a corresponding reference property
1.9. An OO design model for Book and Author
2.1. The complete JavaScript class model
3.1. Two unidirectional associations between Movie and Person.

List of Tables

1.1. An example of an association table
1.2. Different terminologies
1.3. Functionality types
2.1. Sample data for Publisher
2.2. Sample data for Book
3.1. Sample data for Publisher
3.2. Sample data for Book
3.3. Sample data for Author
3.4. Movies
3.5. People

Foreword

This tutorial is Part 4 of our series of six tutorials about model-based development of front-end web applications with plain JavaScript. It shows how to build a web app that takes care of the three object types Book, Publisher and Author as well as of two unidirectional associations:

  1. the association between the classes Book and Publisher assigning a publisher to a book,

  2. the association between the classes Book and Author assigning one or more authors to a book.

The app supports the four standard data management operations (Create/Read/Update/Delete). It extends the example app of part 2 by adding code for handling the unidirectional functional (many-to-one) association between Book and Publisher, and the unidirectional non-functional (many-to-many) association between Book and Author. The other parts of the tutorial are:

  • Part 1: Building a minimal app.

  • Part 2: Handling constraint validation.

  • Part 3: Dealing with enumerations.

  • Part 5: Managing bidirectional associations, such as the associations between books and publishers and between books and authors, not only assigning authors and a publisher to a book, but also the other way around, assigning books to authors and to publishers.

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

You may also want to take a look at our open access book Building Front-End Web Apps with Plain JavaScript, which includes all parts of the tutorial in one document, dealing with multiple object types ("books", "publishers" and "authors") and taking care of constraint validation, enumeration attributes, associations and subtypes/inheritance.

Chapter 1. Reference Properties and Unidirectional Associations

A property defined for an object type, or class, is called a reference property if its values are references that reference an object of another, or of the same, type. For instance, the class Committee shown in Figure Figure 1.1 below has a reference property chair, the values of which are references to objects of type ClubMember.

An association between object types classifies relationships between objects of those types. For instance, the association Committee-has-ClubMember-as-chair, which is visualized as a connection line in the class diagram shown in Figure Figure 1.2 below, classifies the relationships FinanceCommittee-has-PeterMiller-as-chair, RecruitmentCommittee-has-SusanSmith-as-chair and AdvisoryCommittee-has-SarahAnderson-as-chair, where the objects PeterMiller, SusanSmith and SarahAnderson are of type ClubMember, and the objects FinanceCommittee, RecruitmentCommittee and AdvisoryCommittee are of type Committee.

Reference properties correspond to a special form of associations, namely to unidirectional binary associations. While a binary association does, in general, not need to be directional, a reference property represents a binary association that is directed from the property's domain class (where it is defined) to its range class.

In general, associations are relationship types with two or more object types participating in them. An association between two object types is called binary. In this tutorial we only discuss binary associations. For simplicity, we just say 'association' when we actually mean 'binary association'.

Table 1.1. An example of an association table

Committee-has-ClubMember-as-chair
Finance Committee Peter Miller
Recruitment Committee Susan Smith
Advisory Committee Sarah Anderson

While individual relationships (such as FinanceCommittee-has-PeterMiller-as-chair) are important information items in business communication and in information systems, associations (such as Committee-has-ClubMember-as-chair) 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) architecture. Unfortunately, many application development frameworks lack the required 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 1.2. 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
functional association one-to-one, many-to-one or one-to-many relationship type function


We first discuss reference properties, which implicitly represent unidirectional binary associations in an "association-free" class model (a model without any explicit association element).

1. References and Reference Properties

A reference can be either human-readable or an internal object reference. Human-readable references refer to identifiers that are used in human communication, such as the unique names of astronomical bodies, the ISBN of books and the employee numbers of the employees of a company. Internal object references refer to the memory addresses of OOP objects, thus providing an efficient mechanism for accessing objects in the main memory of a computer.

Some languages, like SQL and XML, only support human-readable, but not internal references. In SQL, human-readable references are called foreign keys, and the identifiers they refer to are called primary keys. In XML, human-readable references are called ID references and the corresponding attribute type is IDREF.

Objects in an OO program can be referenced either with the help of human-readable references (such as integer codes) or with internal object references, which are preferable for accessing objects efficiently in main memory. Following the XML terminology, we call human-readable references ID references. We follow the standard naming convention for ID reference properties where an ID reference property defined in a class A and referencing objects of class B has the name b_id using the suffix _id. When we store persistent objects in the form of records or table rows, we need to convert internal object references, stored in properties like publisher, to ID references, stored in properties like publisher_id. This conversion is performed as part of the serialization of the object by assigning the standard identifier value of the referenced object to the ID reference property of the referencing object.

In OO languages, 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 Figure 1.1 below has an attribute name with range String, and a reference property chair with range ClubMember.

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

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

Object-oriented programming languages, such as JavaScript, PHP, Java and C#, 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, in other words, single-valued), having exactly one value, like the property chair in class Committee shown in Figure Figure 1.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.

A reference property can be either single-valued (functional) or multi-valued (non-functional). For instance, the reference property Committee::chair shown in Figure Figure 1.1 is single-valued, since it assigns a unique club member as chair to a club. An example of a multi-valued reference property is provided by the property Book::authors shown in Figure Figure 1.9, “An OO design model for Book and Author below.

Normally, the collection value of a multi-valued reference property is a set of references, implying that the order of the references does not matter. In certain cases, however, the order matters and, consequently, the collection value of such a multi-valued reference property is an ordered set of references, typically implemented as a list.

2. Referential Integrity

References are important information items in our application's database. However, they are only meaningful, when their referential integrity is maintained by the app. This requires that for any reference, there is a referenced object in the database. Consequently, any reference property p with domain class C and range class D comes with a referential integrity constraint that has to be checked whenever

  1. a new object of type C is created,

  2. the value of p is changed for some object of type C,

  3. an object of type D is destroyed.

A referential integrity constraint also implies two change dependencies:

  1. An object creation dependency: an object with a reference to another object can only be created after the referenced object has been created.

  2. An object destruction dependency: an object that is referenced by another object can only be destroyed after

    1. the referencing object is destroyed first (the CASCADE deletion policy), or

    2. the reference in the referencing object is either dropped (the DROP-REFERENCE deletion policy) or replaced by another reference.

    For every reference property in our app's model classes, we have to choose, which of these two possible deletion policies applies.

In certain cases, we may want to relax this strict regime and allow creating objects that have non-referencing values for an ID reference property, but we do not consider such cases.

Typically, object creation dependencies are managed in the user interface by not allowing the user to enter a value of an ID reference property, but only to select one from a list of all existing target objects.

3. Modeling Reference Properties as Unidirectional Associations

A reference property (such as chair in the example shown in Figure Figure 1.1 above) can be modeled in a UML class diagram in the form of an association end owned by its domain class, which is visualized with the help of a small filled circle (also called a "dot"). This requires to connect the domain class and the range class of the reference property with an association line, place an ownership dot at the end of this line at the range class side, and annotate this association end with the property name and with a multiplicity symbol, as shown in Figure Figure 1.2 below for the case of our example. In this way we get a unidirectional association, the source class of which is the property's domain and the target class of which is the property's range.

The fact that an association end is owned by the class at the other end, as visually expressed by the association end ownership dot at the association end chair in the example shown in Figure Figure 1.2 below, implies that the association end represents a reference property. In the example of Figure Figure 1.2, the represented reference property is Committee::chair having ClubMember as range. Such an association, with only one association end ownership dot, is unidirectional in the sense that it allows `navigation´ (object access) in one direction only: from the class at the opposite side of the dot (the source class) to the class where the dot is placed (the target class).

Figure 1.2. An association end with a "dot"

An association end with a "dot"

Thus, the two diagrams shown in Figure Figure 1.1 and Figure Figure 1.2 express essentially equivalent models. When a reference property is modeled by an association end with a "dot", like chair in Figure Figure 1.1, then the property's multiplicity is attached to the association end. Since in a design 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 Figure 1.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 any ClubMember is the chair of a Committee?" for finding the minimum cardinality and the question "can a 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", we can simply write the asterisk symbol *. The association shown in Figure Figure 1.2 assigns at most one object of type ClubMember as chair to an object of type Committee. Consequently, it's an example of a functional association.

The following table provides an overview about the different cases of functionality of an association:

Table 1.3. 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


Notice that the directionality and the functionality type of an association are independent of each other. So, a unidirectional association can be either functional (one-to-one or many-to-one), or non-functional (one-to-many or many-to-many).

4. Representing Unidirectional Associations as Reference Properties

A unidirectional association between a source and a target class can be represented as a reference property of the source class. For the case of a unidirectional one-to-one association, this is illustrated in Figure Figure 1.3 below.

Figure 1.3. Representing the unidirectional association ClubMember has Committee as chairedCommittee as a reference property

Representing the unidirectional association ClubMember has Committee as chairedCommittee as a reference property
Representing the unidirectional association ClubMember has Committee as chairedCommittee as a reference property
Representing the unidirectional association ClubMember has Committee as chairedCommittee as a reference property

Notice that, in a way, we have eliminated the explicit association and replaced it with a corresponding reference property resulting in a class model that can be coded with a classical OOP language in a straightforward way. OOP languages do not support associations as first class citizens. They do not have a language element for defining associations. Consequently, an OOP class design model, which we call OO design model, must not contain explicit associations.

5. Adding Directionality to a Non-Directed Association

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 Figure 1.4 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 realized) 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 1.4. A model of a non-directed association between Committee and ClubMember

A model of a non-directed association between Committee and ClubMember

An information design model without association end ownership dots is acceptable as a relational database design model, but it is incomplete as a design model for OOP languages. For instance, the model of Figure Figure 1.4 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 an information design model of a non-directed association (without any association end ownership dots) into an information design model where all associations are either unidirectional or bidirectional: 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 Figure 1.4 above, we have the following options:

  1. Place an ownership dot at the chair association end, leading to the model shown in Figure Figure 1.2 above, which can be transformed into the OO design model shown in Figure Figure 1.1 above.

  2. Place an ownership dot at the chairedCommittee association end, leading to the completed models shown in Figure Figure 1.3 above.

  3. Make the association bidirectional 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 discussed in the next part of our 5-part tutorial.

    Figure 1.5. Modeling a bidirectional association between Committee and ClubMember

    Modeling a bidirectional association between Committee and ClubMember

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

In the case of a functional association that is not one-to-one, the simplest design is obtained by defining the direction of the association according to its functionality, placing the association end ownership dot at the association end with the multiplicity 0..1 or 1 . For a non-directed one-to-one or many-to-many association, we can choose the direction as we like, that is, we can place the ownership dot at either association end.

6. Our Running Example

The model shown in Figure Figure 1.6 below (about publishers and books) serves as our running example for a unidirectional functional association in this tutorial. Notice that it contains the unidirectional many-to-one association Book-has-Publisher.

Figure 1.6. The Publisher-Book information design model with a unidirectional association

The Publisher-Book information design model with a unidirectional association

We may also have to deal with a non-functional (multi-valued) reference property representing a unidirectional non-functional association. For instance, the unidirectional many-to-many association between Book and Author shown in Figure Figure 1.7 below, models a multi-valued (non-functional) reference property authors.

Figure 1.7. The Publisher-Book-Author information design model with two unidirectional associations

The Publisher-Book-Author information design model with two unidirectional associations

7. Eliminating Unidirectional Associations

Since classical OO programming languages do not support associations as first class citizens, but only classes and reference properties representing unidirectional associations, we have to eliminate all explicit associations from general information design models for obtaining OO design models.

7.1. The basic elimination procedure restricted to unidirectional associations

The starting point of our restricted association elimination procedure is an information design model with various kinds of unidirectional associations, such as the model shown in Figure Figure 1.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, as discussed in Section Section 5.

A unidirectional association connecting a source with a target class is replaced with a corresponding reference property in its source class having

  1. the same name as 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);

  2. the target class as its range;

  3. the same multiplicity as the target association end,

  4. a uniqueness constraint if the unidirectional association is inverse functional.

This replacement procedure is illustrated for the case of a unidirectional one-to-one association in Figure Figure 1.3 above.

For the case of a unidirectional one-to-many association, Figure Figure 1.8 below provides an illustration of the association elimination procedure. Here, the non-functional association end at the target class Point is turned into a corresponding reference property with name points obtained as the pluralized form of the target class name.

Figure 1.8. Turn a non-functional association end into a corresponding reference property


7.2. Eliminating associations from the design model

In the case of our running example, the Publisher-Book-Author information design model, we have to replace both unidirectional associations with suitable reference properties. In the first step, we replace the many-to-one association Book-has-Publisher in the model of Figure Figure 1.6 with a functional reference property publisher in the class Book, resulting in the following OO design model:

Notice that since the target association end of the Book-has-Publisher association has the multiplicity 0..1, we have to declare the new property publisher as optional by appending the multiplicity 0..1 to its name.

In the second step, we replace the many-to-many association Book-has-Author in the model of Figure Figure 1.7 with a multi-valued reference property authors in the class Book, resulting in the following OO design model:

Figure 1.9. An OO design model for Book and Author

An OO design model for Book and Author

After the platform-independent OO 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 coded in the chosen platform. Thus, for any platform considered, two guidelines are needed: 1) how to make the platform-specific data model, and 2) how to code this model.

8. Rendering Reference Properties in the User Interface

The widgets used for data input and output in a (CRUD) data management user interface (UI) normally correspond to properties defined in a model class of an app. We have to distinguish between (various types of) input fields corresponding to (various kinds of) attributes, and choice widgets (such as selection lists) corresponding to enumeration attributes or to reference properties. Representing reference properties in the UI with select controls, instead of input fields, prevents the user from entering invalid ID references, so it takes care of referential integrity.

In general, a single-valued reference property can be rendered as a single-selection list in the UI, no matter how many objects populate the reference property's range, from which one specific choice is to be made. If the cardinality of the reference property's range is sufficiently small (say, not greater than 7), then we can also use a radio button group instead of a selection list.

A multivalued reference property can be rendered as a multiple-selection list in the UI. However, the corresponding multiple-select control of HTML is not really usable as soon as there are many (say, more than 20) different options to choose from because the way it renders the choice is visually too scattered. In the special case of having only a few (say, no more than 7) options, we can also use a checkbox group instead of a multiple-selection list. But for the general case of having in the UI a list containing all associated objects chosen from the reference property's range class, we need to develop a special UI widget that allows to add (and remove) objects to (and from) a list of chosen objects.

Such a multiple-choice widget consists of

  1. an HTML list element containing the chosen (associated) objects, where each list item contains a push button for removing the object from the choice;

  2. a single-select control that, in combination with a push button, allows to add a new associated object from the range of the multi-valued reference property.

Chapter 2. Implementing Unidirectional Functional Associations with Plain JavaScript

The three example apps that we have discussed in previous chapters, the minimal app,the validation app, and the enumeration 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 subtype (inheritance) relationships between object types. Handling associations and subtype relationships are advanced issues in software application engineering. They are often not sufficiently discussed in software development text books and not well supported by application development frameworks. In this part of the tutorial, we show how to deal with unidirectional associations, while bidirectional associations and subtype relationships are covered in parts 5 and 6.

We adopt the approach of model-based development, which provides a general methodology for engineering all kinds of artifacts, including 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 chapter on reference properties and associations, before you continue to read this tutorial chapter, or you start reading this tutorial chapter and consult the theory chapter only on demand, e.g., when you stumble upon a term that you don't know.

A unidirectional functional association is either one-to-one or many-to-one. In both cases such an association is represented, or implemented, with the help of a single-valued reference property.

In this chapter of our tutorial, we show

  1. how to derive a plain JavaScript class model from an OO design model with single-valued reference properties representing unidirectional functional associations,

  2. how to code the JavaScript class model in the form of plain JavaScript model classes,

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

1. Implementing Single-Valued Reference Properties with Plain JavaScript

A single-valued reference property, such as the property publisher of the object type Book, allows storing internal references to objects of another type, such as Publisher. When creating a new object, the constructor function needs to have a parameter for allowing to assign a suitable value to the reference property. In a typed programming language, such as Java, we would have to take a decision if this value is expected to be an internal object reference or an ID reference. In JavaScript, however, we can take a more flexible approach and allow using either of them, as shown in the following example:

class Book {
  constructor (slots) {
    ...
    // assign object reference or ID reference
    this.publisher = slots.publisher || slots.publisher_id;
    ...
  }
  ...
}

Notice that the Book constructor parameter slots may contain either a publisher slot representing a JS object reference or a publisher_id slot representing an ID reference. In JavaScript, we can use a disjunctive expression like slots.publisher || slots.publisher_id for achieving this. We handle the resulting ambiguity in the property setter by checking the type of the argument as shown in the following code fragment:

set publisher( p) {
  var publisher_id = "";
  // p can be an ID reference or an object reference 
  publisher_id = (typeof p !==  "object") ? p : p.name;
  ...
  this._publisher = Publisher.instances[ publisher_id];
  ...
}

Notice that the name of a publisher is used as an ID reference, since it is the standard ID of the Publisher class.

2. Make a JavaScript Class Model

The starting point for making a JavaScript class model is an OO design model like the following one:

How to make this model (by eliminating associations) has been discussed in Chapter 1.

The meaning of the design model and its reference property publisher can be illustrated by a sample data population for the two model classes Book and Publisher:

Table 2.1. Sample data for Publisher

Name Address
Bantam Books New York, USA
Basic Books New York, USA

Table 2.2. Sample data for Book

ISBN Title Year Publisher
0553345842 The Mind's I 1982 Bantam Books
1463794762 The Critique of Pure Reason 2011
1928565379 The Critique of Practical Reason 2009
0465030793 I Am A Strange Loop 2000 Basic Books

We now show how to derive a JavaScript class model from the design model above in four steps. For each class in the design model:

  1. Add a «get/set» stereotype to all (non-derived) single-valued properties, implying that they have implicit getters and setters. Recall that in the setter, the corresponding check operation is invoked and the property is only set, if the check does not detect any constraint violation.

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

    1. A basic check operation, like checkIsbn, for checking all syntactic constraints, but not the mandatory value and the uniqueness constraints.

    2. A standard ID check operation, like checkIsbnAsId, for checking the mandatory value and uniqueness constraints that are implied by a standard ID attribute.

    3. If other classes have a reference property that references the class under consideration, add an ID reference check operation for checking the referential integrity constraint imposed on ID reference (or foreign key) attributes. For instance, since the Book::publisher property references Publisher objects, we need a checkNameAsIdRef operation in the Publisher class.

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

  3. Add an object serialization function toString() for showing an object's state in error messages and log messages.

  4. Add an object-to-storage conversion function toRecord() that prepares a model object for being stored as a row in an entity table, which can be serialized to a JSON string with JSON.stringify such that it can be stored as a value of a key in an app's localStorage datastore.

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

We have to perform a similar transformation also for the class Publisher. This gives us the complete JavaScript class model derived from the above OO design model, as depicted in the following class diagram.

Figure 2.1. The complete JavaScript class model


3. New Issues

Compared to the validation and enumeration apps discussed in Part 2 and Part 3 of tour tutorial, we have to deal with a number of new technical issues:

  1. In the model code we now have to take care of reference properties that require

    1. maintenance of referential integrity;

    2. choosing and implementing one of the two possible deletion policies discussed in Section 2 for managing the corresponding object destruction dependency in the destroy method of the property's range class;

    3. conversion between internal object references and external ID references in the serialization function toString() and the conversion function convertObj2Row(), as well as in the constructor function.

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

    1. showing information about associated objects in the Retrieve/List use case;

    2. allowing to select an object from a list of all existing instances of the association's target class and add it to, or remove an object from, a list of associated objects, in the Create and Update use cases.

4. Write the Model Code

The JavaScript class model can be directly coded for getting the JavaScript model classes of our app.

4.1. Summary

  1. Code each model class as an ES6 class with implicit getters and setters (or as an ES5 constructor function with explicit setters).

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

  3. In each setter, the corresponding property check is invoked and the property is only set, if the check does not detect any constraint violation.

  4. Write the code of the serialization function toString() and the conversion function toRecord().

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

4.2. Code each model class as an ES6 class

Each class C of the JavaScript class model is coded as an ES6 class with the same name C and a constructor having a single record parameter slots, which has a key-value slot for each (non-derived) property of the class. The range of these properties should be indicated in a comment. In the case of a reference property the range is another model class.

In the constructor body, we first assign default values to all properties. The default value of a reference property is null. These default values will be used when the constructor is invoked as a default constructor, that is, without any argument. If the constructor is invoked with a slots argument, the default values may be overwritten by calling the setter methods for all properties.

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

class Publisher {
  constructor (slots) {
    // set the default values for the parameter-free default constructor
    this._name = "";           // String
    this._address = "";        // String
    // is constructor invoked with a non-empty slots argument?
    if (typeof slots === "object" && Object.keys( slots).length > 0) {
      // assign properties by invoking implicit setters
      this.name = slots.name;
      this.address = slots.address;
    }
  }
  ...
};

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.

For each model class C, we define a class-level property C.instances representing the collection of all C instances managed by the application in the form of an entity table (a map of records): This property is initially set to {}. For instance, in the case of the model class Publisher, we define:

Publisher.instances = {};

The Book class from the JavaScript class model is coded in a similar way:

class Book {
  constructor (slots) {
    // set the default values for the parameter-free default constructor
    this._isbn = "";   // string
    this._title = "";  // string
    this._year = 0;          // Number (PositiveInteger)
    this._authors = {};      // map of associated Author objects
    // this._publisher          optional reference property
    // is constructor invoked with a non-empty slots argument?
    if (typeof slots === "object" && Object.keys( slots).length > 0) {
      this.isbn = slots.isbn;
      this.title = slots.title;
      this.year = slots.year;
      // assign object reference or ID reference
      this.authors = slots.authors || slots.authors_id;
      this.publisher = slots.publisher || slots.publisher_id;
    }
  }
  ...
}

Notice that the Book constructor can be invoked either with an object reference slots.publisher or with an ID reference slots.publisher_id. This approach makes using the Book constructor more flexible and more robust.

4.3. Code the property checks

Take care that all constraints of a property as specified in the JavaScript class model are properly coded in its check function, as explained in Part 2 of tour tutorial. Recall that constraint violation (or validation error) classes are defined in the file lib/errorTypes.js.

For instance, for the Publisher.checkName function we obtain the following code:

class Publisher {
  ...
  static checkName( n) {
    if (n === undefined) {
      return new NoConstraintViolation();  // not mandatory
    } else {
      if (typeof n !== "string" || n.trim() === "") {
        return new RangeConstraintViolation(
		    "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 constraints in checkName, and check the mandatory value and uniqueness constraints in checkNameAsId, which invokes checkName:

  static checkNameAsId( n) {
    var constraintViolation = Publisher.checkName(n);
    if ((constraintViolation instanceof NoConstraintViolation)) {
      if (n === undefined) {
        return new MandatoryValueConstraintViolation(
            "A publisher name is required!");
      } else if (Publisher.instances[String(n)]) { 
        constraintViolation = new UniquenessConstraintViolation(
            "There is already a publisher record with this name!");
      } else {
        constraintViolation = new NoConstraintViolation();
      }
    }
    return constraintViolation;
  }

If we have to deal with ID references (foreign keys) in other classes, we need to provide a further check function, called checkNameAsIdRef, for checking the referential integrity constraint:

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

The condition (!Publisher.instances[n]) checks if there is no publisher object with the given name n, and then creates a constraintViolation object. This referential integrity constraint check is used by the Book.checkPublisher function:

class Book {
  ...
  static checkPublisher( publisher_id) {
    var constraintViolation = null;
    if (!publisher_id) {
      constraintViolation = new NoConstraintViolation();  // optional
    } else {
      // invoke foreign key constraint check
      constraintViolation = Publisher.checkNameAsIdRef( publisher_id);
    }
    return constraintViolation;
  }
  ...
}

4.4. Code the property setters

In the setters, the corresponding check function is invoked and the property is only set, if the check does not detect any constraint violation. In the case of a reference property, we allow invoking the setter either with an object reference or with an ID reference. The resulting ambiguity is resolved by testing if the argument provided in the invocation of the setter is an object or not. For instance, the publisher setter is coded in the following way:

class Book {
  ...
  set publisher( p) {
    var constraintViolation = null;
    var publisher_id = "";
    // p can be an ID reference or an object reference
    publisher_id = (typeof p !== "object") ? p : p.name;
    constraintViolation = Book.checkPublisher( publisher_id);
    if (constraintViolation instanceof NoConstraintViolation) {
      // create the new publisher reference
      this._publisher = Publisher.instances[ publisher_id];
    } else {
      throw constraintViolation;
    }
  }
  ...
}

4.5. Choose and implement a deletion policy

For any reference property, we have to choose and implement one of the two possible deletion policies discussed in Section 2 for managing the corresponding object destruction dependency in the destroy method of the property's range class. In our case, when deleting a publisher record, we have to choose between

  1. deleting all records of books published by the deleted publisher (Existential Dependency);

  2. dropping from all books published by the deleted publisher the reference to the deleted publisher (Existential Independence).

We choose the second option. This is shown in the following code of the Publisher.destroy method where for all concerned book objects book the property book.publisher is cleared:

Publisher.destroy = function (name) {
  var publisher = Publisher.instances[name];
  var book=null, keys=[], i=0;
  // delete all references to this publisher in book objects
  keys = Object.keys( Book.instances);
  for (i=0; i < keys.length; i++) {
    book = Book.instances[keys[i]];
    if (book.publisher === publisher) delete book.publisher;
  }
  // delete the publisher record
  delete Publisher.instances[name];
  console.log("Publisher " + name + " deleted.");
};

Notice that the deletion of all references to the deleted publisher is performed in a sequential scan through all book objects. which may be inefficient when there are many of them. It would be much more efficient when each publisher object would hold a list of references to all books published by this publisher. Creating and maintaining such a list would make the association between books and their publisher bidirectional.

4.6. Serialization and Object-to-Record Mapping

In the case of a reference property, like Book::publisher, the serialization function toString() has to show a human-readable identifier of the referenced object, like this.publisher.name:

class Book {
  ...
  toString() {
    var bookStr = "Book{ ISBN:"+ this.isbn +", title:"+ this.title +
        ", year:"+ this.year;
    if (this.publisher) bookStr += ", publisher:"+ this.publisher.name;
    return bookStr + "}";
  }

The object-to-record conversion function toRecord converts typed objects with object references to corresponding (untyped) record objects with ID references:

  toRecord() {
    var rec = {};
    Object.keys( this).forEach( function (p) {
      // copy only property slots with underscore prefix
      if (p.charAt(0) === "_") {
        switch (p) {
        case "_publisher":
          // convert object reference to ID reference
          if (this._publisher) rec.publisher_id = this._publisher.name;
          break;
        default:
          // remove underscore prefix
          rec[p.substr(1)] = this[p];
        }
      }
    }, this);
    return rec;
  }
}

The inverse conversion, from untyped record objects with ID references to corresponding typed objects with object references, is performed by the Book constructor, which tolerates both ID references and object references as arguments for setting reference properties.

5. Write the View and Controller Code

The user interface (UI) consists of a start page index.html that allows navigating to data management UI pages, one for each object type (in our example, books.html and publishers.html). Each of these data management UI pages contains 5 sections: a Manage section, like Manage books, with a menu for choosing a CRUD use case, and a section for each CRUD use case, like Retrieve/list all books, Create book, Update book and Delete book, such that only one of them is displayed at any time (for instance, by setting the CSS property display:none for all others).

5.1. Initialize the app

For initializing a data management use case, the required data (for instance, all publisher and book records) have to be loaded from persistent storage. This is performed in a controller procedure such as pl.c.books.manage.initialize in c/books.js with the following code:

pl.c.books.manage = {
  initialize: function () {
    Publisher.retrieveAll();
    Book.retrieveAll();
    pl.v.books.manage.setUpUserInterface();
  }
};

The initialize method for managing book data first loads the publishers table and the books table since the book data management UI needs to show their data. Then the book data management menu is rendered by calling the setUpUserInterface procedure.

5.2. Show associated objects in the Retrieve/List All use case

In our example, we have only one reference property, Book::publisher, which is functional and optional. For showing information about the optional publisher of a book in the Retrieve/list all use case, the corresponding cell in the HTML table is filled with the name of the publisher, if there is any:

pl.v.books.retrieveAndListAll = {
  setupUserInterface: function () {
    var tableBodyEl = document.querySelector(
                      "section#Book-R>table>tbody");
    var keys = Object.keys( Book.instances);
    var row=null, listEl=null, book=null;
    tableBodyEl.innerHTML = "";
    for (var i=0; i < keys.length; i++) {
      book = Book.instances[keys[i]];
      row = tableBodyEl.insertRow(-1);
      row.insertCell(-1).textContent = book.isbn;
      row.insertCell(-1).textContent = book.title;
      row.insertCell(-1).textContent = book.year;
      row.insertCell(-1).textContent = 
          book.publisher ? book.publisher.name : "";
    }
    document.getElementById("Book-M").style.display = "none";
    document.getElementById("Book-R").style.display = "block";
  }
};

For a multi-valued reference property, the table cell would have to be filled with a list of all associated objects referenced by the property.

5.3. Selecting associated objects in the Create and Update use cases

For allowing to select objects to be associated with the currently edited object from in the Create and Update use cases, an HTML selection list (i.e., a select element) is populated with option elements formed from the instances of the associated object type with the help of a utility method fillSelectWithOptions. The HTML select element is defined in the books.html view file:

<section id="Book-C" class="UI-Page">
 <h1>Public Library: Create a new book record</h1>
 <form>
  ... 
  <div class="select-one">
   <label>Publisher: <select name="selectPublisher"></select></label>
  </div>
  ...
 </form>
</section>

The Create user interface is set up by the following procedure:

pl.v.books.create = {
  setupUserInterface: function () {
    var formEl = document.querySelector("section#Book-C>form"),
        publisherSelectEl = formEl.selectPublisher,
        saveButton = formEl.commit;
    // define event handlers for responsive validation 
    formEl.isbn.addEventListener("input", function () {
      formEl.isbn.setCustomValidity( 
          Book.checkIsbnAsId( formEl.isbn.value).message);
    });
    // set up the publisher selection list
    util.fillSelectWithOptions( publisherSelectEl, 
        Publisher.instances, "name");
    // define event handler for saveButton click events    
    saveButton.addEventListener("click", 
        this.handleSaveButtonClickEvent);
    // define event handler for neutralizing the submit event
    formEl.addEventListener("submit", function (e) { 
      e.preventDefault();
      formEl.reset();
    });
    // replace the manage form with the create form
    document.getElementById("Book-M").style.display = "none";
    document.getElementById("Book-C").style.display = "block";
    formEl.reset();
  },
  handleSaveButtonClickEvent: function () {
    ...
  }
};

When the user clicks (or touches) the save button, all form control values, including the value of the select control, are copied to a slots list, which is used as the argument for invoking the add method after all form fields have been checked for validity, as shown in the following program listing:

  handleSaveButtonClickEvent: function () {
    var formEl = document.querySelector("section#Book-C>form");
    var slots = {
        isbn: formEl.isbn.value, 
        title: formEl.title.value,
        year: formEl.year.value,
        publisherIdRef: formEl.selectPublisher.value
    };
    // check input fields and show constraint violation error messages 
    formEl.isbn.setCustomValidity( Book.checkIsbnAsId( slots.isbn).message);
    /* ... (do the same with title and year) */
    // save the input data only if all of the form fields are valid
    if (formEl.checkValidity()) {
      Book.add( slots);
    }
  }

The setupUserInterface code for the update book use case is similar.

Chapter 3. Implementing Unidirectional Non-Functional Associations with Plain JavaScript

A unidirectional non-functional association is either one-to-many or many-to-many. In both cases such an association is represented, or implemented, with the help of a multi-valued reference property.

In this chapter, we show

  1. how to derive a JavaScript class model from an OO design model with multi-valued reference properties representing unidirectional non-functional associations,

  2. how to code the JavaScript class model in the form of JavaScript model classes,

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

1. Implementing Multi-Valued Reference Properties in JavaScript

A multivalued reference property, such as the property authors of the object type Book, allows storing a set of references to objects of some type, such as Author. When creating a new object of type Book, the constructor function needs to have a parameter for providing a suitable value for this reference property. In JavaScript we can allow this value to be a set of internal object references or of ID references, as shown in the following example:

class Book {
  constructor (slots) {
    // set the default values for the parameter-free default constructor
    ...
    this._authors = {};      // map of associated Author objects
    // is constructor invoked with a non-empty slots argument?
    if (typeof slots === "object" && Object.keys( slots).length > 0) {
      ...
      // assign object references or ID references
      this.authors = slots.authors || slots.authorIdRefs;
    }
  }
}

Notice that the constructor parameter slots is expected to contain either an authors or an authorIdRefs slot. The JavaScript expression slots.authors || slots.authorIdRefs, using the disjunction operator ||, evaluates to authors, if slots contains a slot with property name authors, or to authorIdRefs, otherwise. We handle the resulting ambiguity in the property setter by checking the type of the argument as shown in the following code fragment:

  set authors( a) {
    var keys=[], i=0;
    this._authors = {};
    if (Array.isArray(a)) {  // array of IdRefs
      for (i=0; i < a.length; i++) {
        this.addAuthor( a[i]);
      }
    } else {  // map of object references
      keys = Object.keys( a);
      for (i=0; i < keys.length; i++) {
        this.addAuthor( a[keys[i]]);
      }
    }
  }

In JS, a set-valued reference property can be implemented in two ways:

  1. with a list (a JS array) of object references as its value,

  2. with a map of object references as its value, such that the values of the object's standard ID attribute are the keys of the map.

We prefer using maps for implementing set-valued reference properties since they guarantee that each element is unique, while with an array we would have to prevent duplicate elements. Also, an element of a map can be easily deleted (with the help of the delete operator), while this requires more effort in the case of an array. However, for implementing ordered association ends corresponding to ordered-collection-valued reference properties, we use JS arrays.

2. Make a JavaScript Class Model

The starting point for making a JavaScript class model is an OO design model where reference properties represent associations. The following model for our example app contains the multi-valued reference property Book::authors, which represents the unidirectional many-to-many association Book-has-Author:

The meaning of the design model and its reference properties publisher and authors can be illustrated by a sample data population for the three model classes:

Table 3.1. Sample data for Publisher

Name Address
Bantam Books New York, USA
Basic Books New York, USA

Table 3.2. Sample data for Book

ISBN Title Year Authors Publisher
0553345842 The Mind's I 1982 1, 2 Bantam Books
1463794762 The Critique of Pure Reason 2011 3
1928565379 The Critique of Practical Reason 2009 3
0465030793 I Am A Strange Loop 2000 2 Basic Books

Table 3.3. Sample data for Author

Author ID Name
1 Daniel Dennett
2 Douglas Hofstadter
3 Immanuel Kant

For deriving a JS class model from the OO design model we have to follow the same steps as in Section 2 and, in addition, we have to take care of multi-valued reference properties, such as Book::authors, for which we.

  1. create a class-level check operation, such as checkAuthor, which is responsible for checking the corresponding referential integrity constraint for the references to be added to the property's collection;

  2. Create an add operation, such as addAuthor, for adding a reference to the collection;

  3. Create a remove operation, such as removeAuthor, for removing a reference from the collection.

This leads to the following JS class model:

3. New issues

Compared to dealing with a functional association, as discussed in the previous chapter, we now have to deal with the following new technical issues:

  1. In the model code we now have to take care of multi-valued reference properties that require implementing

    1. an add and a remove method, such as addAuthor and removeAuthor, as well as a setter method for assigning a set of object references with the help of the add method, possibly converting ID references to object references; all three methods may need to check cardinality constraints, if there are any;

    2. a class-level check operation, such as checkAuthor, which has to invoke the corresponding check...AsIdRef method of the property's range class for checking the property's implicit referential integrity constraint;

    3. converting an entity table (a map of typed object references) to a list (JS array) of ID references in the conversion function toRecord.

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

    1. showing information about a set of associated objects in the property's column of the table view of the Retrieve/list all use case; the simplest approach is showing a comma-separated list of ID references, possibly combined with corresponding names; alternatively, HTML lists can be rendered in the property's table data cells;

    2. allowing to select a set of associated objects from a list of all existing instances of the property's range class in the Create and Update use cases.

    The last issue, allowing to select a set of associated objects from a list of all instances of some class, can, in general, not be solved with the help of an HTML multiple-select form control because of its poor usability. Whenever the set of selectable options is greater than a certain threshold (defined by the number of options that can be seen on the screen without scrolling), the multiple-select element is no longer usable, and an alternative multiple-choice widget has to be used.

4. Write the Model Code

Notice that, for simplicity, we do not include the code for all validation checks shown in the JS class model in the code of the example app.

4.1. Code the add and remove operations

For the multi-valued reference property Book::authors, we need to code the operations addAuthor and removeAuthor. Both operations accept one parameter denoting an author either by ID reference (the author ID as integer or string) or by an object reference. The code of addAuthor is as follows:

  addAuthor( a) {
    var constraintViolation=null,
        authorIdRef=0, key="";
    // a can be an ID reference or an object reference
    authorIdRef = (typeof a !== "object") ? parseInt( a) : a.authorId;
    constraintViolation = Book.checkAuthor( authorIdRef);
    if (authorIdRef &&
        constraintViolation instanceof NoConstraintViolation) {
      // add the new author reference
      key = String( authorIdRef);
      this._authors[key] = Author.instances[key];
    } else {
      throw constraintViolation;
    }
  }

In the removeAuthor method, the author reference is first checked and, if no constraint violation is detected, the corresponding entry in the map this._authors is deleted:

  removeAuthor( a) {
    var constraintViolation=null, authorIdRef="";
    // a can be an ID reference or an object reference
    authorIdRef = (typeof a !== "object") ? parseInt( a) : a.authorId;
    constraintViolation = Book.checkAuthor( authorIdRef);
    if (constraintViolation instanceof NoConstraintViolation) {
      // delete the author reference
      delete this._authors[String( authorIdRef)];
    } else {
      throw constraintViolation;
    }
  }

For assigning an array of ID references, or a map of object references, to the property Book::authors, the method setAuthors adds them one by one with the help of addAuthor:

  set authors( a) {
    var keys=[], i=0;
    this._authors = {};
    if (Array.isArray(a)) {  // array of IdRefs
      for (i=0; i < a.length; i++) {
        this.addAuthor( a[i]);
      }
    } else {  // map of object references
      keys = Object.keys( a);
      for (i=0; i < keys.length; i++) {
        this.addAuthor( a[keys[i]]);
      }
    }
  }

4.2. Choose and implement a deletion policy

For the reference property Book::authors, we have to choose and implement a deletion policy in the destroy method of the Author class. We have to choose between

  1. deleting all books (co-)authored by the deleted author (Existential Dependency);

  2. dropping from all books (co-)authored by the deleted author the reference to the deleted author (Existential Independence).

We go for the second option. This is shown in the following code of the Author.destroy method where for all concerned book objects book the author reference book.authors[authorKey] is dropped:

Author.destroy = function (id) {
  var authorKey = String(id),
      author = Author.instances[authorKey],
      key="", keys=[], book=null, i=0;
  // delete all dependent book records
  keys = Object.keys( Book.instances);
  for (i=0; i < keys.length; i++) {
    key = keys[i];
    book = Book.instances[key];
    if (book.authors[authorKey]) delete book.authors[authorKey];
  }
  // delete the author object
  delete Author.instances[authorKey];
  console.log("Author " + author.name + " deleted.");
};

4.3. Serialization and Object-to-Record Mapping

We need a serialization function toString() for converting an object to a human-readable string representation that can be used for showing an object in a user interface, and an object-to-record mapping function toRecord() converting a typed object to a corresponding record that can be saved in a persistent datastore. In both cases, internal object references are converted to ID references.

The toString() function creates a string representation that may only contain the relevant properties. The simplest method for showing a set of associated objects, like the authors of a book, is creating a comma-separated list of IDs:

class Book {
  ...
  toString() {
    var bookStr = "Book{ ISBN:"+ this.isbn + ", title:" + 
        this.title + ", year:" + this.year;
    if (this.publisher) {
      bookStr += ", publisher:" + this.publisher.name;
    }
    return bookStr +", authors:" + 
        Object.keys( this.authors).join(",") +"}";
  }

The toRecord() function needs to serialize all properties of an object, deleting the underscore prefix for obtaining the corresponding record field name:

   toRecord() {
    var row = {};
    Object.keys( this).forEach( function (p) {
      // copy only property slots with underscore prefix
      if (p.charAt(0) === "_") {
        switch (p) {
        case "_publisher":
          // convert object reference to ID reference
          if (this._publisher) row.publisher_id = this._publisher.name;
          break;
        case "_authors":
          // convert the map of object references to a list of ID references
          row.authorIdRefs = [];
          Object.keys( this.authors).forEach( function (authorIdStr) {
            row.authorIdRefs.push( parseInt( authorIdStr));
          });
          break;
        default:
          row[p.substr(1)] = this[p];  // remove underscore prefix
        }
      }
    }, this);
    return row;
  }

5. Write the View Code

5.1. Show information about associated objects in the Retrieve/List All use case

For showing information about the authors of a book in the Retrieve/List All use case, the corresponding cell in the HTML table is filled with a list of the names of all authors with the help of the utility function util.createListFromMap:

pl.v.books.retrieveAndListAll = {
  setupUserInterface: function () {
    var tableBodyEl = document.querySelector(
	                  "section#Book-R>table>tbody");
    var i=0, row=null, book=null, listEl=null, 
	    keys = Object.keys( Book.instances);
    tableBodyEl.innerHTML = "";  // drop old contents
    for (i=0; i < keys.length; i++) {
      book = Book.instances[keys[i]];
      row = tableBodyEl.insertRow(-1);
      row.insertCell(-1).textContent = book.isbn;
      row.insertCell(-1).textContent = book.title;
      row.insertCell(-1).textContent = book.year;
      // create list of authors
      listEl = util.createListFromMap( book.authors, "name");
      row.insertCell(-1).appendChild( listEl);
      // if the book has a publisher, show its name
      row.insertCell(-1).textContent = 
	      book.publisher ? book.publisher.name : "";
    }
    document.getElementById("Book-M").style.display = "none";
    document.getElementById("Book-R").style.display = "block";
  }
};

The utility function util.createListFromMap has the following code:

createListFromMap: function (m, displayProp) {
  var listEl = document.createElement("ul");
  util.fillListFromMap( listEl, m, displayProp);
  return listEl;
},
fillListFromMap: function (listEl, m, displayProp) {
  var keys=[], listItemEl=null;
  // drop old contents
  listEl.innerHTML = "";
  // create list items from object property values
  keys = Object.keys( m);
  for (var j=0; j < keys.length; j++) {
    listItemEl = document.createElement("li");
    listItemEl.textContent = m[keys[j]][displayProp];
    listEl.appendChild( listItemEl);
  }
},

5.2. Selecting associated objects in the Create use case

For allowing to select multiple authors to be associated with the currently edited book in the Create use case, a multiple selection list (a select element with multiple="multiple"), as shown in the HTML code below, is populated with the instances of the associated object type.

<section id="Book-C" class="UI-Page">
 <h1>Public Library: Create a new book record</h1>
 <form>
  ... 
  <div class="select-one">
   <label>Publisher: <select name="selectPublisher"></select></label>
  </div>
  <div class="select-many">
   <label>Authors: 
    <select name="selectAuthors" multiple="multiple"></select>
   </label>
  </div>
  ...
 </form>
</section>

The Create UI is set up by populating selection lists for selecting the authors and the publisher with the help of a utility method fillSelectWithOptions as shown in the following program listing:

pl.v.books.create = {
  setupUserInterface: function () {
    var formEl = document.querySelector("section#Book-C > form"),
        authorsSelectEl = formEl.selectAuthors,
        publisherSelectEl = formEl.selectPublisher,
        submitButton = formEl.commit;
    // define event handlers for form field input events
      ...
    // set up the (multiple) authors selection list
    util.fillSelectWithOptions( authorsSelectEl, 
        Author.instances, "authorId", {displayProp:"name"});
    // set up the publisher selection list
    util.fillSelectWithOptions( publisherSelectEl, 
        Publisher.instances, "name");
    ...
  },
  handleSaveButtonClickEvent: function () {
    ...
  }
};

When the user clicks the save button, all form control values, including the value of any single-select control, are copied to a corresponding field of the slots record, which is used as the argument for invoking the add method after all form fields have been checked for validity. Before invoking add, we first have to create (in the authorIdRefs slot) a list of author ID references from the selected options of the multiple authors selection list, as shown in the following program listing:

  handleSaveButtonClickEvent: function () {
    var i=0, 
        formEl = document.querySelector("section#Book-C > form"),
        selectedAuthorsOptions = formEl.selectAuthors.selectedOptions;
    var slots = {
        isbn: formEl.isbn.value, 
        title: formEl.title.value,
        year: formEl.year.value,
        authorIdRefs: [],
        publisher_id: formEl.selectPublisher.value
    };
    // check all input fields 
    ...
    // save the input data only if all of the form fields are valid
    if (formEl.checkValidity()) {
      // construct the list of author ID references
      for (i=0; i < selectedAuthorsOptions.length; i++) {
        slots.authorIdRefs.push( selectedAuthorsOptions[i].value);
      } 
      Book.add( slots);
    }
  }

The Update use case is discussed in the next section.

5.3. Selecting associated objects in the Update use case

Unfortunately, the multiple-select control is not really usable for displaying and allowing to maintain the set of associated authors in realistic use cases where we have several hundreds or thousands of authors, because the way it renders the choice is visually too scattered. So we have to use a special multiple-choice widget that allows to add (and remove) objects to (and from) a list of associated objects, as discussed in Section 8. In order to show how this widget can replace the multiple-selection list discussed in the previous section, we use it now in the Update use case.

For allowing to maintain the set of authors associated with the currently edited book in the Update use case, a multiple-choice widget as shown in the HTML code below, is populated with the instances of the Author class.

<section id="Book-U" class="UI-Page">
  <h1>Public Library: Update a book record</h1>
  <form>
    ...
    <div class="select-one">
      <label>Publisher: <select name="selectPublisher"></select></label>
    </div>
    <div class="widget">
      <label for="updBookSelectAuthors">Authors: </label>
      <div class="MultiSelectionWidget" id="updBookSelectAuthors"></div>
    </div>
    ...
  </form>
</section>

The Update user interface is set up (in the setupUserInterface procedure shown below) by populating the selection list for selecting the book to be updated with the help of the utility method fillSelectWithOptions. The selection list for assigning a publisher and the multiple-choice widget for assigning the authors of a book are only populated (in handleSubmitButtonClickEvent) when a book to be updated has been chosen.

pl.v.books.update = {
  setupUserInterface: function () {
    var formEl = document.querySelector("section#Book-U > form"),
        bookSelectEl = formEl.selectBook,
        submitButton = formEl.commit;
    // set up the book selection list
    util.fillSelectWithOptions( bookSelectEl, Book.instances, 
        "isbn", {displayProp:"title"});
    bookSelectEl.addEventListener("change", this.handleBookSelectChangeEvent);
    ... 
    // define event handler for save button click events    
    submitButton.addEventListener("click", this.handleSaveButtonClickEvent);
    // define event handler for neutralizing the submit event and reseting the form
    formEl.addEventListener( 'submit', function (e) {
      var authorsSelWidget = document.querySelector(
              "section#Book-U > form .MultiSelectionWidget");
      e.preventDefault();
      authorsSelWidget.innerHTML = "";
      formEl.reset();
    });
    document.getElementById("Book-M").style.display = "none";
    document.getElementById("Book-U").style.display = "block";
    formEl.reset();
  },

When a book to be updated has been chosen, the form input fields isbn, title and year, and the select control for updating the publisher, are assigned corresponding values from the chosen book, and the associated authors selection widget is set up:

  handleBookSelectChangeEvent: function () {
    var formEl = document.querySelector("section#Book-U > form"),
        authorsSelWidget = formEl.querySelector(
            ".MultiSelectionWidget"),
        publisherSelectEl = formEl.selectPublisher,
        key = formEl.selectBook.value,
        book=null;
    if (key !== "") {
      book = Book.instances[key];
      formEl.isbn.value = book.isbn;
      formEl.title.value = book.title;
      formEl.year.value = book.year;
      // set up a multiple-choice widget for associated authors
      util.createMultiSelectionWidget( authorsSelWidget, 
          book.authors, Author.instances, "authorId", "name");
      // set up the associated publisher selection list
      util.fillSelectWithOptions( publisherSelectEl, 
          Publisher.instances, "name");
      // assign associated publisher to index of select element  
      formEl.selectPublisher.selectedIndex = 
          (book.publisher) ? book.publisher.index : 0;
    } else {
      formEl.reset();
      formEl.selectPublisher.selectedIndex = 0;
    }
  },

When the user, after updating some values, finally clicks the save button, all form control values, including the value of the single-select control for assigning a publisher, are copied to corresponding slots in a slots record variable, which is used as the argument for invoking the update method after all values have been checked for validity. Before invoking update, a list of ID references to authors to be added, and another list of ID references to authors to be removed, is created (in the authorIdRefsToAdd and authorIdRefsToRemove slots) from the updates that have been recorded in the associated authors selection widget with the help of classList values, as shown in the following program listing:

  handleSaveButtonClickEvent: function () {
    var assocAuthorListItemEl=null, i=0,
        authorIdRefsToAdd=[], authorIdRefsToRemove=[],
        formEl = document.querySelector("section#Book-U > form"),
        selectBookEl = formEl.selectBook,
        authorsSelWidget = formEl.querySelector(
            ".MultiChoiceWidget"),
        authorsAssocListEl = authorsSelWidget.firstElementChild;
    var slots = { isbn: formEl.isbn.value, 
          title: formEl.title.value,
          year: formEl.year.value,
          publisher_id: formEl.selectPublisher.value
        };
    // commit the update only if all form field values are valid
    if (formEl.checkValidity()) {
      // construct authorIdRefs-ToAdd/ToRemove lists 
      for (i=0; i < authorsAssocListEl.children.length; i++) {
        assocAuthorListItemEl = authorsAssocListEl.children[i]; 
        if (assocAuthorListItemEl.classList.contains("removed")) {
          authorIdRefsToRemove.push( 
              assocAuthorListItemEl.getAttribute("data-value"));          
        }
        if (assocAuthorListItemEl.classList.contains("added")) {
          authorIdRefsToAdd.push( 
              assocAuthorListItemEl.getAttribute("data-value"));          
        }
      } 
      // if the add/remove list is non-empty create a slot
      if (authorIdRefsToRemove.length > 0) {
        slots.authorIdRefsToRemove = authorIdRefsToRemove;
      }
      if (authorIdRefsToAdd.length > 0) {
        slots.authorIdRefsToAdd = authorIdRefsToAdd;
      }
      Book.update( slots);
      // update the book selection list's option element
      selectBookEl.options[selectBookEl.selectedIndex].text = 
          slots.title;
    }
  }

6. How to Run the App, Get the Code and Ask Questions

You can run the example app from our server, download it as a ZIP archive file and ask questions about the concept of unidirectional associations and how to implement them on our discussion forum.

7. Points of Attention

We have still included the repetitive code structures (called boilerplate code) in the model layer per class and per property for constraint validation (checks and setters) and per class for the data storage management methods add, update, and destroy. While it is good to write this code a few times for learning app development, you don't want to write it again and again later when you work on real projects. In Part 6, we will present an approach how to put these methods in a generic form in a meta-class called mODELcLASS, such that they can be reused in all model classes of an app.

8. Practice Project

This project is based on the information design model shown below. The app from the previous assignments is to be extended by adding the possibility to manage data about the actors and the director of a movie. This is achieved by adding a model class Person and two unidirectional associations between Movie and Person:

  1. a many-to-one association assigning exactly one person as the director of a movie, and

  2. a many-to-many association assigning zero or more persons as the actors of a movie.

Figure 3.1. Two unidirectional associations between Movie and Person.

Two unidirectional associations between Movie and Person.

This project includes the following tasks:

  1. Make an OO design model derived from the given information design model.

  2. Make a JavaScript class model derived from the association-fee design model.

  3. Code your JavaScript class model, following the guidelines of the tutorial.

You can use the following sample data for testing your app:

Table 3.4. Movies

Movie ID Title Release date Director Actors
1 Pulp Fiction 1994-05-12 3 5, 6
2 Star Wars 1977-05-25 2 7, 8
3 Dangerous Liaisons 1988-12-16 1 9, 5

Table 3.5. People

Person ID Name
1 Stephen Frears
2 George Lucas
3 Quentin Tarantino
5 Uma Thurman
6 John Travolta
7 Ewan McGregor
8 Natalie Portman
9 Keanu Reeves

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

If you have any questions about this project, you can ask them on our discussion forum.