Introduction
This article has been extracted from a six-part tutorial about building front-end web applications with plain JavaScript available as the open access book Building Front-End Web Apps with Plain JavaScript. It shows how to build a front-end web app where model classes have enumeration attributes, which have to be rendered in the HTML user interface with selection lists, radio button groups and checkbox groups. If you first want to see how it works and how it looks like, you can run the enumeration app discussed in this article from our server.
The data management app discussed in this tutorial takes care of only one object type ("books
") and supports the four standard data management operations (Create/Read/Update/Delete), but it needs to be enhanced by adding further important parts of the app's overall functionality:
- Part 4: Managing unidirectional associations assigning authors and publishers to books
- Part 5: Managing bidirectional associations also assigning books to authors and to publishers
- Part 6: Handling subtype (inheritance) relationships in class hierarchies
Background
If you didn't read them already, you may first want to read parts 1 and 2 of this tutorial: Building a Minimal App in Seven Steps and Adding Constraint Validation.
In all application domains, there are string
-valued attributes with a fixed set of possible string
values. These attributes are called enumeration attributes, and the fixed value sets defining their possible string
values are called enumerations. For instance, when we have to manage data about persons, we often need to include information about the gender of a person. The possible values of a gender
attribute may be restricted to one of the enumeration labels "male
","female
" and "undetermined
", or to one of the enumeration codes "M
", "F
" and "U
". Whenever we deal with codes, we also need to have their corresponding labels, at least in a legend explaining the meaning of each code.
Instead of using the enumeration string
values as the internal values of an enumeration attribute (such as gender
), it is preferable to use a simplified internal representation for them, such as the positive integers 1, 2, 3, etc., which enumerate the possible values. However, since these integers do not reveal their meaning (which is indicated by the enumeration label) in program code, for readability, we rather use special constants, called enumeration literals, such as MALE
or M
, prefixed by the name of the enumeration, like GenderEL
, in program statements like this.gender = GenderEL.MALE
. Notice that we follow the convention that the names of enumeration literals are written all upper case, and that we also use the convention to suffix the name of an enumeration data type with "EL
" standing for "enumeration literal" (such that we can recognize from the name GenderEL
that each instance of this datatype is a "gender enumeration literal").
In summary, we can distinguish between the following two forms of enumerations:
-
Simple enumerations define a fixed set of self-explanatory enumeration labels, like in the example of a GenderEL
enumeration shown in the UML class diagram to the right. The labels of a simple enumeration are being used, in capitalized form, as the names of the corresponding enumeration literals (GenderEL.MALE
, GenderEL.FEMALE
, etc.).
-
Code lists define a fixed set of code/label pairs. In the case of a code list, we can use both the codes or the labels as the names of enumeration literals, but using the codes seems preferable for brevity (GenderEL.M
, GenderEL.F
, etc.). For displaying the value of an enumeration attribute, it's an option to show not only the label, but also the code, like "male (M)", provided that there is sufficient space. If space is an issue, only the code can be shown.
Notice that, since enumerations are used as the range of enumeration attributes, they are considered to be data types.
Enumerations may have further features. For instance, we may want to be able to define a new enumeration by extending an existing enumeration. In programming languages and in other computational languages, enumerations are implemented with different features in different ways. See also the Wikipedia article on enumerations.
Enumeration Attributes
An enumeration attribute is an attribute that has an enumeration as its range.
In the user interface, an output field for an enumeration attribute would display the enumeration label, rather than its internal value, the corresponding enumeration index.
For allowing user input to an enumeration attribute, we can use the UI concept of a (drop-down) selection list, which may be implemented with an HTML select
element, such that the enumeration labels would be used as the text content of its option
elements, while the enumeration indexes would be used as their values. We have to distinguish between single-valued and multi-valued enumeration attributes. In the case of a single-valued enumeration attribute, we use a standard select
element. In the case of a multi-valued enumeration attribute, we use a select
element with the HTML attribute setting multiple="multiple"
. For both cases, an example is shown in Figure 1.3. While the single select
element for "Original language" shows the initially selected option "---" denoting "nothing selected", the multiple select
element "Other available languages" shows a small window displaying four of the options that can be selected.
For usability, the multiple selection list can only be implemented with an HTML select
element, if the number of enumeration literals does not exceed a certain threshold (like 20), which depends on the number of options the user can see on the screen without scrolling.
For user input to a single-valued enumeration attribute, a radio button group can be used instead of a single selection list, if the number of enumeration literals is sufficiently small (say, not larger than 7). A radio button group is implemented with a HTML fieldset
element acting as a container of labeled input
elements of type "radio", all having the same name, which is normally equal to the name of the represented enumeration attribute.
For user input to a multi-valued enumeration attribute, a checkbox group can be used instead of a multiple selection list, if the number of enumeration literals is sufficiently small (say, not larger than 7). A checkbox group is implemented with a HTML fieldset
element acting as a container of labeled input
elements of type "checkbox", all having the same name, which is normally equal to the name of the represented enumeration attribute.
Coding the App
In this section, we show how to build a front-end web application with enumeration attributes, using plain JavaScript. In addition to the topic of enumeration attributes, we also show how to deal with multi-valued attributes.
We again consider the simple data management problem that was considered in Parts 1 and 2 of this tutorial. So, again, the purpose of our app is to manage information about books. But now we have four additional enumeration attributes, as shown in the UML class diagram below:
- the single-valued mandatory attribute
originalLanguage
with enumeration datatype LanguageEL
- the multi-valued optional attribute
otherAvailableLanguages
with enumeration datatype LanguageEL
- the single-valued mandatory attribute
category
with enumeration datatype BookCategoryEL
- the multi-valued mandatory attribute
publicationForms
with enumeration datatype PublicationFormEL
Notice that the attributes otherAvailableLanguages
and publicationForms
are multi-valued, as indicated by their multiplicity expressions [*] and [1..*]. This means that the possible values of these attributes are sets of enumeration literals, such as the set {ePub, PDF}, which can be represented in JavaScript as a corresponding array list of enmueration literals, [PublicationFormEL.EPUB, PublicationFormEL.PDF]
.
In terms of coding, the new issues are:
-
In the model code, we have to take care of:
- defining the enumeration datatypes with the help of a utility (meta-)class
Enumeration
, which is discussed below: - defining the single-valued enumeration attributes (like
Book::originalLanguage
) together with their checks and setters (like Book.checkOriginalLanguage
, Book.prototype.setOriginalLanguage
) - defining the multi-valued enumeration attributes (like
Book::publicationForms
) together with their checks and setters (like Book.checkPublicationForms
, Book.prototype.setPublicationForms
and Book.prototype.addPublicationForm
) - extending the methods
Book.update
, and Book.prototype.toString
such that they take care of the added enumeration attributes
-
In the user interface code, we have to take care of:
- adding new table columns in listBooks.html and suitable form controls (such as selection lists, radio button groups or checkbox groups) in createBook.html and upateBook.html
- creating output for the new attributes in the method
pl.view.listBooks.setupUserInterface()
; - allowing input for the new attributes in the methods:
pl.view.createBook.setupUserInterface()
and pl.view.updateBook.setupUserInterface()
.
The Meta-Class Enumeration
We define an Enumeration
meta-class, which supports both simple enumerations and code lists, but not record enumerations. While a simple enumeration is defined by a list of labels in the form of a JS array as the constructor argument such that the labels are used for the names of the enumeration literals, a code list is defined as a special kind of key-value map in the form of a JS object as the constructor arguiment such that the codes are used for the names of the enumeration literals. Consequently, the constructor needs to test if the invocation argument is an array or an object. The following first part of the code shows how simple enumerations are created:
function Enumeration( enumArg) {
var i=0, lbl="", LBL="";
if (Array.isArray( enumArg)) {
if (!enumArg.every( function (n) {
return (typeof(n) === "string"); })) {
throw new OtherConstraintViolation(
"A list of enumeration labels must be an array of strings!");
}
this.labels = enumArg;
this.enumLitNames = this.labels;
this.codeList = null;
} else if (typeof(enumArg) === "object" &&
Object.keys( enumArg).length > 0) {
...
} else {
throw new OtherConstraintViolation(
"Invalid Enumeration constructor argument: "+ enumArg);
}
this.MAX = this.enumLitNames.length;
for (i=1; i <= this.enumLitNames.length; i++) {
lbl = this.enumLitNames[i-1].replace(/( |-)/g, "_");
LBL = lbl.split("_").map( function (lblPart) {
return lblPart.toUpperCase();
}).join("_");
this[LBL] = i;
}
Object.freeze( this);
};
Notice that a label like "text book
" or "text-book
" is converted to the enumeration literal name "TEXT_BOOK
". The following second part of the code shows how code list enumerations are created:
function Enumeration( enumArg) {
var i=0, lbl="", LBL="";
if (Array.isArray( enumArg)) {
...
} else if (typeof(enumArg) === "object" &&
Object.keys( enumArg).length > 0) {
if (!Object.keys( enumArg).every( function (code) {
return (typeof( enumArg[code]) === "string"); })) {
throw new OtherConstraintViolation(
"All values of a code list map must be strings!");
}
this.codeList = enumArg;
this.enumLitNames = Object.keys( this.codeList);
this.labels = this.enumLitNames.map( function (c) {
return enumArg[c] +" ("+ c +")";
});
} else {
throw new OtherConstraintViolation(
"Invalid Enumeration constructor argument: "+ enumArg);
}
...
};
Write the Model Code
Encode the Enumerations
Enumerations are encoded in the following way with the help of the meta-class Enumeration
:
var PublicationFormEL = new Enumeration(["hardcover","paperback","ePub","PDF"])
var LanguageEL = new Enumeration({"en":"English", "de":"German",
"fr":"French", "es":"Spanish"})
Notice that LanguageEL
defines a code list, while PublicationFormEL
defines a simple enumeration.
Encode the Model Class as a Constructor Function
The class Book
is encoded by means of a corresponding JavaScript constructor function such that all its (non-derived) properties are supplied with values from corresponding key-value slots of a slots
parameter.
function Book( slots) {
this.isbn = "";
this.title = "";
this.originalLanguage = 0;
this.otherAvailableLanguages = [];
this.category = 0;
this.publicationForms = [];
if (arguments.length > 0) {
this.setIsbn( slots.isbn);
this.setTitle( slots.title);
this.setOriginalLanguage( slots.originalLanguage);
this.setOtherAvailableLanguages( slots.otherAvailableLanguages);
this.setCategory( slots.category);
this.setPublicationForms( slots.publicationForms);
}
};
Encode the Enumeration Attribute Checks
Encode the enumeration attribute checks in the form of class-level ('static
') functions that check if the argument is a valid enumeration index not smaller than 1 and not greater than the enumeration's MAX
value. For instance, for the checkOriginalLanguage
function, we obtain the following code:
Book.checkOriginalLanguage = function (l) {
if (l === undefined) {
return new MandatoryValueConstraintViolation(
"An original language must be provided!");
} else if (!Number.isInteger( l) || l < 1 || l > LanguageEL.MAX) {
return new RangeConstraintViolation("Invalid value for original language: "+ l);
} else {
return new NoConstraintViolation();
}
};
For a multi-valued enumeration attribute, such as publicationForms
, we break down the check code into two functions, one for checking if a value is a valid enumeration index (checkPublicationForm
), and another one for checking if all members of a set of values are valid enumeration indexes (checkPublicationForms
):
Book.checkPublicationForm = function (p) {
if (!p && p !== 0) {
return new MandatoryValueConstraintViolation(
"No publication form provided!");
} else if (!Number.isInteger( p) || p < 1 ||
p > PublicationFormEL.MAX) {
return new RangeConstraintViolation(
"Invalid value for publication form: "+ p);
} else {
return new NoConstraintViolation();
}
};
Book.checkPublicationForms = function (pubForms) {
var i=0, constrVio=null;
if (pubForms.length === 0) {
return new MandatoryValueConstraintViolation(
"No publication form provided!");
} else {
for (i=0; i < pubForms.length; i++) {
constrVio = Book.checkPublicationForm( pubForms[i]);
if (!(constrVio instanceof NoConstraintViolation)) {
return constrVio;
}
}
return new NoConstraintViolation();
}
};
Encode the Enumeration Attribute Setters
Both for single-valued and for multi-valued enumeration attributes, an ordinary setter is defined. In the case of a multi-valued enumeration attribute, this setter assigns an entire set of values (in the form of a JS array) to the attribute after checking its validity.
Write a Serialization Function
The object serialization function now needs to include the values of enumeration attributes:
Book.prototype.toString = function () {
return "Book{ ISBN:"+ this.isbn +", title:"+ this.title +
", originalLanguage:"+ this.originalLanguage +
", otherAvailableLanguages:"+ this.otherAvailableLanguages.toString() +
", category:"+ this.category +
", publicationForms:"+ this.publicationForms.toString() +"}";
};
Notice that for multi-valued enumeration attributes, we call the toString()
function that is predefined for JS arrays.
Data Management Operations
There are only two new issues in the data management operations compared to the validation app:
- We have to make sure that the
util.cloneObject
method, which is used in Book.update
, takes care of copying array-valued attributes, which we didn't have before (in the validation app). - In the
Book.update
method, we now have to check if the values of array-valued attributes have changed, which requires to test if two arrays are equal or not. For code readability, we add an array equality test method to Array.prototype
in browserShims.js
, like so:
Array.prototype.isEqualTo = function (a2) {
return (this.length === a2.length) && this.every( function( el, i) {
return el === a2[i]; });
};
This allows us to express these tests in the following way:
if (!book.publicationForms.isEqualTo( slots.publicationForms)) {
book.setPublicationForms( slots.publicationForms);
updatedProperties.push("publicationForms");
}
Creating Test Data
In the test data records that are created by Book.createTestData
, we now have to provide values for single- and multi-valued enumeration attributes. For readability, we use enumeration literals instead of enumeration indexes:
Book.createTestData = function () {
try {
Book.instances["006251587X"] = new Book({isbn:"006251587X",
title:"Weaving the Web", originalLanguage:LanguageEL.EN,
otherAvailableLanguages:[LanguageEL.DE,LanguageEL.FR],
category:BookCategoryEL.NOVEL,
publicationForms:[PublicationFormEL.EPUB,PublicationFormEL.PDF]});
...
Book.saveAll();
} catch (e) {
console.log( e.constructor.name + ": " + e.message);
}
};
Write the View and Controller Code
Selection Lists
We use HTML selection lists for rendering the enumeration attributes originalLanguage
and otherAvailableLanguages
in the HTML forms in createBook.html
and upateBook.html
. Since the attribute otherAvailableLanguages
is multi-valued, we need a multiple selection list for it, as shown in the following HTML code:
<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="ol">Original language</label>
<select id="ol" name="originalLanguage"></select>
</div>
<div class="pure-control-group">
<label for="oal">Other available languages</label>
<select id="oal" name="otherAvailableLanguages"
multiple="multiple"></select>
</div>
...
</form>
</body>
While we define the select
container elements for these selection lists in createBook.html
and upateBook.html
, we fill in their option
child elements dynamically in the setupUserInterface
methods in view/createBook.js
and view/updateBook.js
with the help of the utility method util.fillSelectWithOptions
. In the case of a single select
element, the user's single-valued selection can be retrieved from the value
attribute of the select
element, while in the case of a multiple select
element, the user's multi-valued selection can be retrieved from the selectedOptions
attribute of the select
element.
Choice Widgets
Since the enumeration attributes category
and publicationForms
have no more than seven possible values, we can use a radio button group and a checkbox group for rendering them in an HTML-form-based UI. Such choice widgets are formed with the help of the container element fieldset
as shown in the following HTML fragment:
<body>
<h1>Public Library: Create a new book record</h1>
<form id="Book" class="pure-form pure-form-aligned">
...
<fieldset class="pure-controls" data-bind="category">
<legend>Category</legend>
</fieldset>
<fieldset class="pure-controls" data-bind="publicationForms">
<legend>Publication forms</legend>
</fieldset>
<div class="pure-controls">
<p><button type="submit" name="commit">Save</button></p>
<nav><a href="index.html">Back to main menu</a></nav>
</div>
</form>
</body>
Notice that we use a custom attribute data-bind
for indicating to which attribute of the underlying model class the choice widget is bound. In the same way as the option
child elements of a selection list, also the label
ed input
child elements of a choice widget are created dynamically with the help of the utility method util.createChoiceWidget
in the setupUserInterface
methods in view/createBook.js
and view/updateBook.js
. Like a selection list implemented with the HTML select
element that povides the user's selection in the value
or selectedOptions
attribute, our choice widgets also need a DOM attribute that holds the user's single- or multi-valued choice. We dynamically add a custom attribute data-value
to the choice widget's fieldset
element for this purpose in util.createChoiceWidget
.
setupUserInterface: function () {
var formEl = document.forms['Book'],
origLangSelEl = formEl.originalLanguage,
otherAvailLangSelEl = formEl.otherAvailableLanguages,
categoryFieldsetEl = formEl.querySelector(
"fieldset[data-bind='category']"),
pubFormsFieldsetEl = formEl.querySelector(
"fieldset[data-bind='publicationForms']"),
submitButton = formEl.commit;
util.fillSelectWithOptions( origLangSelEl, LanguageEL.labels);
util.fillSelectWithOptions( otherAvailLangSelEl, LanguageEL.labels);
util.createChoiceWidget( categoryFieldsetEl, "category", [],
"radio", BookCategoryEL.labels);
util.createChoiceWidget( pubFormsFieldsetEl, "publicationForms", [],
"checkbox", PublicationFormEL.labels);
...
},
Responsive Validation for Selection Lists and Choice Widgets
Since selection lists and choice widgets do not allow arbitrary user input, we do not have to check constraints such as range constraints or pattern constraints on user input, but only mandatory value constraints. In our example app, the enumeration attributes originalLanguage
, category
and publicationForms
are mandatory, while otherAvailableLanguages
is optional.
For selection lists, whenever a new selection is made by the user, a change
event is raised by the browser, so we declare the following mandatory value check as an event listener for change
events on the select
element:
setupUserInterface: function () {
...
formEl.title.addEventListener("input", function () {
formEl.title.setCustomValidity(
Book.checkTitle( formEl.title.value).message);
});
origLangSelEl.addEventListener("change", function () {
origLangSelEl.setCustomValidity(
(!origLangSelEl.value) ? "A value must be selected!":"" );
});
categoryFieldsetEl.addEventListener("click", function () {
formEl.category[0].setCustomValidity(
(!categoryFieldsetEl.getAttribute("data-value")) ?
"A category must be selected!":"" );
});
pubFormsFieldsetEl.addEventListener("click", function () {
var val = pubFormsFieldsetEl.getAttribute("data-value");
formEl.publicationForms[0].setCustomValidity(
(!val || Array.isArray(val) && val.length === 0) ?
"At least one publication form must be selected!":"" );
});
...
},
Practice Project
The purpose of the app to be built is managing information about movies. The app deals with just one object type, Movie
, and with two enumerations, as depicted in the following class diagram.
First, make a list of all the constraints that have been expressed in this model. Then, code the app by following the guidance of this tutorial and the Validation Tutorial.
Compared to the practice project of our validation tutorial, two attributes have been added: the optional single-valued enumeration attribute rating
, and the multi-valued enumeration attribute genres
.
If you have any questions about this project, you can post them below, in the comments section.
History
- June 30, 2015: Added section "Practice Project"
- May 22, 2015: First version