In this chapter, we show how to build a minimal front-end web application with plain JavaScript and Local Storage. The purpose of our example app is to manage information about books. That is, we deal with a single object type: Book
, as depicted in the class diagram of Figure 3.1.
The following table shows a sample data population for the model class Book
:
Table 3.1 A collection of book objects represented as a table
ISBN | Title | Year |
006251587X | Weaving the Web | 2000 |
0465026567 | Gödel, Escher, Bach | 1999 |
0465030793 | I Am A Strange Loop | 2008 |
What do we need for a data management app? There are four standard use cases, which have to be supported by the app:
These four standard use cases, and the corresponding data management operations, are often summarized with the acronym CRUD.
For entering data with the help of the keyboard and the screen of our computer, we use HTML forms, which provide the user interface technology for web applications.
For maintaining a collection of persistent data objects, we need a storage technology that allows to keep data objects in persistent records on a secondary storage device, such as a hard-disk or a solid state disk. Modern web browsers provide two such technologies: the simpler one is called Local Storage, and the more powerful one is called IndexedDB51. For our minimal example app, we use Local Storage.
In the first step, we set up our folder structure for the application. We pick a name for our app, such as “Public Library”, and a corresponding (possibly abbreviated) name for the application folder, such as “PublicLibrary” or MinimalApp
. Then we create this folder on our computer’s disk and a subfolder “src” for our JavaScript source code files. In this folder, we create the subfolders “m”, “v” and “c”, following the Model-View-Controller paradigm for software application architectures. And finally we create an index. html
file for the app’s start page, as discussed below. Thus, we end up with the following folder structure:
MinimalApp
In the start page HTML file of the app, we load the file initialize.js
and the Book. js
model class file:
The start page provides a menu for choosing one of the CRUD data management use cases. Each use case is performed by a corresponding page such as, for instance, createBook.html
. The menu also contains options for creating test data with the help of the procedure Book.createTestData()
and for clearing all data with Book. clearData()
:
In the second step, we write the code of our model class and save it in a specific model class file. In an MVC app, the model code is the most important part of the app. It’s also the basis for writing the view and controller code. In fact, large parts of the view and controller code could be automatically generated from the model code. Many MVC frameworks provide this kind of code generation.
In the information design model shown in Figure 3.1 above, there is only one class, representing the object type Book
. So, in the folder src/m
, we create a file Book.js
that initially contains the following code:
The model class Book
is coded as a JavaScript constructor function with a single slots
parameter, which is a record object with fields isbn
, title
and year
, representing the constructor parameters to be assigned to the ISBN, the title and the year attributes of the class Book
. Notice that, for getting a simple name, we have put the class name Book
in the global scope, which is okay for a small app with only a few classes. In general, however, we should use the model namespace for model classes, which requires class/constructor definitions like
In addition to defining the model class in the form of a constructor function, we also define the following items in the Book. js
file:
Book. instances
representing the collection of all Book
instances managed by the application in the form of an entity table.Book. retrieveAll
for loading all managed Book instances from the persistent data store.Book. saveAll
for saving all managed Book instances to the persistent data store.Book. add
for creating a new Book instance.Book. update
for updating an existing Book instance.Book. destroy
for deleting a Book instance.Book. createTestData
for creating a few example book records to be used as test data.Book. clearData
for clearing the book datastore.For representing the collection of all Book instances managed by the application, we define and initialize the class-level property Book. instances
in the following way:
So, initially our collection of books is empty. In fact, it’s defined as an empty object literal, since we want to represent it in the form of an entity table (a map of entity records) where an ISBN is a key for accessing the corresponding book record (as the value associated with the key). We can visualize the structure of an entity table in the form of a lookup table:
Key | Value |
006251587X | { isbn:”006251587X,” title:”Weaving the Web”, year:2000} |
0465026567 | { isbn:”0465026567,” title:”Gödel, Escher, Bach”, year:1999} |
0465030793 | { isbn:”0465030793,” title:”I Am A Strange Loop”, year:2008} |
Notice that the values of such a map are records corresponding to table rows. Consequently, we could also represent them in a simple table, as shown in Table 3.1.
The Book. add
procedure takes care of creating a new Book
instance and adding it to the Book.instances
collection:
For persistent data storage, we use the Local Storage API supported by modern web browsers. Loading the book records from Local Storage involves three steps:
1.Retrieving the book table that has been stored as a large string with the key “books” from Local Storage with the help of the assignment
2.Converting the book table string into a corresponding entity table books
with book rows as elements, with the help of the built-in procedure JSON.parse
:
This conversion is called de-serialization.
3.Converting each row of books
, representing a record (an untyped object), into a corresponding object of type Book
stored as an element of the entity table Book.instances
, with the help of the procedure convertRec2Obj
defined as a “static” (class-level) method in the Book
class:
Here is the full code of the procedure:
Notice that since an input operation like localStorage["books"]
may fail, we perform it in a try-catch block, where we can follow up with an error message whenever the input operation fails.
For updating an existing Book
instance we first retrieve it from Book. instances
, and then re-assign those attributes the value of which has changed:
A Book instance is deleted from the entity table Book. instances
by first testing if the table has a row with the given key (line 2), and then applying the JavaScript built-in delete
operator, which deletes a slot from an object, or an entry from a map:
Saving all book objects from the Book. instances
collection in main memory to Local Storage in secondary memory involves two steps:
1. Converting the entity table Book. instances
into a string with the help of the predefined JavaScript procedure JSON.stringify
:
This conversion is called serialization.
2. Writing the resulting string as the value of the key “books” to Local Storage:
These two steps are performed in line 5 and in line 6 of the following program listing:
For being able to test our code, we may create some test data and save it in our Local Storage database. We can use the following procedure for this:
The following procedure clears all data from Local Storage:
We initialize the application by defining its namespace and MVC sub-namespaces. Namespaces are an important concept in software engineering and many programming languages, including Java and PHP, provide specific support for namespaces, which help grouping related pieces of code and avoiding name conflicts. Since there is no specific support for namespaces in JavaScript, we use special objects for this purpose (we may call them “namespace objects”). First we define a root namespace (object) for our app, and then we define three sub-namespaces, one for each of the three parts of the application code: model, view and controller. In the case of our example app, we may use the following code for this:
Here, the main namespace is defined to be pl
, standing for “Public Library”, with the three sub-namespaces m
, v
and c
being initially empty objects. We put this code in a separate file initialize.js
in the c
folder, because such a namespace definition belongs to the controller part of the application code.
For our example app, the user interface page for the CRUD use case Create is called createBook. html
located in the MinimalApp
folder. In its head
element, it loads the app initialization file initialize. js
, the model class file Book. js
and the view code file createBook. js
, and adds a load
event listener for setting up the Create user interface:
For a data management use case with user input, such as “Create”, an HTML form is required as a user interface. The form typically has a labelled input
or select
field for each attribute of the model class:
The view code file src/ v/createBook. js
contains two procedures:
setupUserInterface
takes care of retrieving the collection of all objects from the persistent data store and setting up an event handler (handleSaveButtonClickEvent
) on the save button for handling click button events by saving the user input data;handleSaveButtonClickEvent
reads the user input data from the form fields and then saves this data by calling the Book. add
procedure.The user interface for the CRUD use case Retrieve consists of an HTML table for displaying the data of all model objects. For our example app, this page is called retrieveAndListAllBooks.html
, located in the main folder MinimalApp
, and it contains the following code in its head
element:
Notice that, in addition to loading the app initialization JS file and the model class JS file, we load the view code file (here: retrieveAndListAllBooks.js
) and invoke its setupUserInterface
procedure via a load
event listener. This is the pattern we use for all four CRUD use cases.
In the setupUserInterface
procedure, we first set up the data management context by retrieving all book data from the database and then fill the table by creating a table row for each book object from Book. instances
:
More specifically, the procedure setupUserInterface
creates the view table in a loop over all objects of Book. instances
. In each step of this loop, a new row is created in the table body element with the help of the JavaScript DOM operation insertRow()
, and then three cells are created in this row with the help of the DOM operation insertCell()
: the first one for the isbn
property value of the book object, and the second and third ones for its title
and year
property values. Both insertRow
and insertCell
have to be invoked with the argument -1 for making sure that new elements are appended to the list of rows and cells.
Also for the Update use case, we have an HTML page for the user interface (updateBook.html
) and a view code file (src/v/updateBook.js
). The HTML form for the UI of the “update book” operation has a selection field for choosing the book to be updated, an output
field for the standard identifier attribute isbn
, and an input
field for each attribute of the Book
class that can be updated. Notice that by using an output
field for the standard identifier attribute, we do not allow changing the standard identifier of an existing object.
Notice that we include a kind of empty option element, with a value of ""
and a display text of—, as a default choice in the selectBook
selection list element. So, by default, the value
of the selectBook
form control is empty, requiring the user to choose one of the available options for filling the form.
The setupUserInterface
procedure now has to populate the select
element’s option list by loading the collection of all book objects from the data store and creating an option element for each book object:
A book selection event is caught via a listener for change
events on the select
element. When a book is selected, the form is filled with its data:
When the save button is activated, a slots
record is created from the form field values and used as the argument for calling Book. update
:
The user interface for the Delete use case just has a select
field for choosing the book to be deleted:
Like in the Update case, the setupUserInterface
procedure in the view code in src/ v/deleteBook. js
loads the book data into main memory, populates the book selection list and adds some event listeners. The event handler for Delete button click events.
You can run the minimal app52 from our server or download the code53 as a ZIP archive file.
Instead of using the Local Storage API, the IndexedDB54 API could be used for locally storing the application data. With Local Storage you only have one database (which you may have to share with other apps from the same domain) and there is no support for database tables (we have worked around this limitation in our approach). With IndexedDB you can set up a specific database for your app, and you can define database tables, called ’object stores’, which may have indexes for accessing records with the help of an indexed attribute instead of the standard identifier attribute. Also, since IndexedDB supports larger databases, its access methods are asynchronous and can only be invoked in the context of a database transaction.
Alternatively, for remotely storing the application data with the help of a web API one can either use a back-end solution component or a cloud storage service. The remote storage approach allows managing larger databases and supports multi-user apps.
For simplicity, we have used raw HTML without any CSS styling. But a user interface should be appealing. So, the code of this app should be extended by adding suitable CSS style rules.
Today, the UI pages of a web app have to be adaptive (frequently called “responsive”) for being rendered on different devices with different screen sizes and resolutions, which can be detected with CSS media queries. The main issue of an adaptive UI is to have a fluid layout, in addition to proper viewport settings. Whenever images are used in a UI, we also need an approach for adaptive bitmap images: serving images in smaller sizes for smaller screens and in higher resolutions for high resolution screens, while preferring scalable SVG images for diagrams and artwork. In addition, we may decrease the font-size of headings and suppress unimportant content items on smaller screens.
For our purposes, and for keeping things simple, we customize the adaptive web page design defined by the HTML5 Boilerplate55 project (more precisely, the minimal “responsive” configuration available on www.initializr.com). It just consists of an HTML template file and two CSS files: the browser style normalization file normalize.css
(in its minified form) and a main.css
, which contains the HTML5 Boilerplate style and our customizations. Consequently, we use a new css
subfolder containing these two CSS files:
One customization change we have made in index.html
is to replace the <div class="main">
container element with the new HTML 5.1 element <main>
such that we obtain a simple and clear UI page structure provided by the sequence of the three container elements <header>
, <main>
and <footer>
. This change in the HTML file requires corresponding changes in main.css
. In addition, we define our own styles for <table>
, <menu>
and <form>
elements. Concerning the styling of HTML forms, we define a simple style for implicitly labeled form control elements.
The start page index.html
now must take care of loading the CSS page styling files with the help of the following two link
elements:
Since the styling of user interfaces is not our primary concern, we do not discuss the details of it and leave it to our readers to take a closer look. You can run the CSS-styled minimal app56 from our server or download its code57 as a ZIP archive file.
The app discussed in this chapter is limited to support the minimum functionality of a data management app only. It does not take care of preventing users from entering invalid data into the app’s database. In Chapter 8, we show how to express integrity constraints in a model class, and how to perform data validation both in the model/ storage code of the app and in the HTML5-based user interface.
Notice that in this chapter, we have made the assumption that all application data can be loaded into main memory (like all book data is loaded into the map Book. instances
). This approach only works in the case of local data storage of smaller databases, say, with not more than 2 MB of data, roughly corresponding to 10 tables with an average population of 1000 rows, each having an average size of 200 Bytes. When larger databases are to be managed, or when data is stored remotely, it’s no longer possible to load the entire population of all tables into main memory, but we have to use a technique where only parts of the table contents are loaded.
Another issue with the do-it-yourself code of this example app is the boilerplate code needed per model class for the data storage management methods add
, retrieve
, 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 Volume 2, we present an approach how to put these methods in a generic form in a meta-class, such that they can be reused in all model classes of an app.
Serializing an attribute value means to convert it to a suitable string value. For standard datatypes, such as numbers, a standard serialization is provided by the predefined conversion function String
. When a string value, like “13” or “yes”, represents the value of a non-string-valued attribute, it has to be de-serialized, that is, converted to the range type of the attribute, before it is assigned to the attribute. This is the situation, for instance, when a user has entered a value in a form input field for an integer-valued attribute. The value of the form field is of type string, so it has to be converted (de-serialized) to an integer using the predefined conversion function parseInt
.
For instance, in our example app, we have the integer-valued attribute year
. When the user has entered a value for this attribute in a corresponding form field, in the Create or Update user interface, the form field holds a string value, which has to be converted to an integer in an assignment like the following:
One important question is: where should we take care of de-serialization: in the “view” (before the value is passed to the “model” layer), or in the “model”? Since attribute range types are a business concern, and the business logic of an app is supposed to be encapsulated in the “model”, de-serialization should be performed in the “model” layer, and not in the “view”.
The explicit labeling of form fields requires to add an id
value to the input
element and a for
-reference to its label
element as in the following example:
This technique for associating a label with a form field is getting quite inconvenient when we have many form fields on a page because we have to make up a great many of unique id
values and have to make sure that they don’t conflict with any of the id
values of other elements on the same page. It’s therefore preferable to use an approach, called implicit labeling, where these id
references are not needed. In this approach, the input
element is a child element of its label
element, as in
Having input
elements as child elements of their label
elements doesn’t seem very logical. Rather, one would expect the label
to be a child of an input
element. But that’s the way it is defined in HTML5.
A small disadvantage of using implicit labels may be the lack of support by certain CSS libraries. In the following parts of this tutorial, we will use our own CSS styling for implicitly labeled form fields.
When an app is used by more than one user at the same time, we have to take care of somehow synchronizing the possibly concurrent read/write actions of users such that users always have current data in their “views” and are prevented from interfering with each other. This is a very difficult problem, which is attacked in different ways by different approaches. It has been mainly investigated for multi-user database management systems and large enterprise applications built on top of them.
The original MVC proposal included a data binding mechanism for automated one-way model-to-view synchronization (updating the model’s views whenever a change in the model data occurs). We didn’t take care of this in our minimal app because a front-end app with local storage doesn’t really have multiple concurrent users. However, we can create a (rather artificial) situation that illustrates the issue:
updateLearningUnit.html
twice), such that you get two browser tabs rendering the same page.A mechanism for automatically updating all views of a model object whenever a change in its property values occurs is provided by the observer pattern that treats any view as an observer of its model object. Applying the observer pattern requires that (1) model objects can have a multi-valued reference property like observers, which holds a set of references to view objects; (2) a notify method can be invoked on view objects by the model object whenever one of its property values is changed; and (3) the notify method defined for view objects takes care of refreshing the user interface.
Notice, however, that the general model-view synchronization problem is not really solved by automatically updating all (other users’) views of a model object whenever a change in its data occurs. Because this would only help, if the users of these views didn’t make themselves any change of the data item concerned, meanwhile. Otherwise, their changed data value would be overwritten by the automated refresh, and they may not even notice this, which is not acceptable in terms of usability.
From an architectural point of view, it is important to keep the app’s model classes independent of
In this chapter, we have kept the model class Book
independent of the UI code, since it does not contain any references to UI elements, nor does it invoke any view method. However, for simplicity, we didn’t keep it independent of storage management code, since we have included the method definitions for add, update, destroy, etc., which invoke the storage management methods of JavaScrpt’s localStorage
API. Therefore, the separation of concerns is incomplete in our minimal example app.
We show in Volume 2 how to achieve a more complete separation of concerns by defining abstract storage management methods in a special storage manager class, which is complemented by libraries of concrete storage management methods for specific storage technologies, called storage adapters.
In most parts of the following projects you can follow, or even copy, the code of the book data management app presented in this chapter. Like in the book data management app, you can make the simplifying assumption that all the data can be kept in main memory. So, on application start up, the data is read from the persistent data store. When the user quits the application, the data has to be saved to the persistent data store, which should be implemented with JavaScript’s Local Storage API, as shown in this chapter, or with the more powerful IndexedDB58 API.
For developing the apps, simply follow the sequence of seven steps described above:
Preset
to “HTML5 + SVG 1. 1 + MathML 3.0”),If you have any questions about how to carry out the following projects, you can ask them on our discussion forum62.
The purpose of the app to be developed is managing information about movies. The app deals with just one object type: Movie
, as depicted in the following class diagram:
Notice that in the Movie
class there is an attribute with range Date
, which is a special datatype, discussed in Chapter 13.
You can use the sample data shown in Table 3.2 for testing your app.
Table 3.2 Sample data about movies
Movie ID | Title | Release date |
1 | Pulp Fiction | 1994–05–12 |
2 | Star Wars | 1977–05–25 |
3 | Casablanca | 1943–01–23 |
4 | The Godfather | 1972–03–15 |
More movie data can be found on the IMDb website63.
Variation: Improve your app by replacing the use of the localStorage
API for persistent data storage with using the more powerful IndexedDB64 API.
The purpose of the app to be developed is managing information about countries. The app deals with just one object type: Country
, as depicted in the following class diagram:
You can use the sample data shown below in Table 3.3 for testing your app.
Table 3.3 Sample data about countries
Name | Population | Life expectancy |
Germany | 80,854,408 | 80.57 |
France | 66,553,766 | 81.75 |
Russia | 142,423,773 | 70.47 |
Monaco | 30,535 | 89.52 |
More data about countries can be found in the CIA World Factbook65.