3Building a Minimal Web App with Plain JS in Seven Steps

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.

Figure 3.1 The object type Book.

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:

  1. Create a new book record by allowing the user to enter the data of a book that is to be added to the collection of stored book records.
  2. Retrieve (or read) all books from the data store and show them in the form of a list.
  3. Update the data of a book record.
  4. Delete a book record.

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.

3.1Step 1Set up the Folder Structure

In the first step, we set up our folder structure for the application. We pick a name for our app, such as “Public Library”, and a corresponding (possibly abbreviated) name for the application folder, such as “PublicLibrary” 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():

3.2Step 2Write the Model Code

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:

  1. A class-level property Book. instances representing the collection of all Book instances managed by the application in the form of an entity table.
  2. A class-level method Book. retrieveAll for loading all managed Book instances from the persistent data store.
  3. A class-level method Book. saveAll for saving all managed Book instances to the persistent data store.
  4. A class-level method Book. add for creating a new Book instance.
  5. A class-level method Book. update for updating an existing Book instance.
  6. A class-level method Book. destroy for deleting a Book instance.
  7. A class-level method Book. createTestData for creating a few example book records to be used as test data.
  8. A class-level method Book. clearData for clearing the book datastore.

3.2.1Representing the collection of all Book instances

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

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.

3.2.2Creating a new Book instance

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

3.2.3Loading all Book instances

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.

3.2.4Updating a Book instance

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

3.2.5Deleting a Book instance

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:

3.2.6Saving all Book instances

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

1. Converting the 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:

3.2.7Creating test data

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

3.2.8Clearing all data

The following procedure clears all data from Local Storage:

3.3Step 3Initialize the Application

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.

3.4Step 4Implement the Create Use Case

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:

  1. 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;
  2. handleSaveButtonClickEvent reads the user input data from the form fields and then saves this data by calling the Book. add procedure.

3.5Step 5Implement the Retrieve/List All Use Case

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.

3.6Step 6Implement the Update Use Case

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:

3.7Step 7Implement the Delete Use Case

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.

3.8Run the App and Get the Code

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

3.9Possible Variations and Extensions

3.9.1Using IndexedDB as an Alternative to LocalStorage

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.

3.9.2Styling the User Interface

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.

3.10Points of Attention

3.10.1Catching invalid data

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.

3.10.2Database size and memory management

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.

3.10.3Boilerplate code

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.

3.10.4Serializing and de-serializing attribute values

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

3.10.5Implicit versus explicit form field labels

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.

3.10.6Synchronizing views with the model

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:

  1. Open the Update UI page of the minimal app twice (for instance, by opening updateLearningUnit.html twice), such that you get two browser tabs rendering the same page.
  2. Select the same learning unit on both tabs, such that you see its data in the Update view.
  3. Change one data item of this learning unit on one of the tabs and save your change.
  4. When you now go to the other tab, you still see the old data value, while you may have expected that it would have been automatically updated.

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.

3.10.7Architectural separation of concerns

From an architectural point of view, it is important to keep the app’s model classes independent of

  1. the user interface (UI) code because it should be possible to re-use the same model classes with different UI technologies;
  2. the storage management code because it should be possible to re-use the same model classes with different storage technologies.

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.

3.11Practice Projects

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:

  1. Step 1 – Set up the Folder Structure
  2. Step 2 – Write the Model Code
  3. Step 3 – Initialize the Application
  4. Step 4 – Implement the Retrieve/List All Objects Use Case
  5. Step 5 – Implement the Create Object Use Case
  6. Step 6 – Implement the Update Object Use Case
  7. Step 7 – Implement the Delete Object Use Case

Also make sure that

  1. your HTML pages comply with the XML syntax of HTML5, preferably by checking with XHTML5 validation59(setting the validator field Preset to “HTML5 + SVG 1. 1 + MathML 3.0”),
  2. international characters are supported by using UTF-8 encoding for all HTML files,
  3. your JavaScript code complies with our Coding Guidelines60 and its style is checked with JSHint61(for instance, instead of the unsafe equality test with “==”, always the strict equality test with “===” has to be used).

If you have any questions about how to carry out the following projects, you can ask them on our discussion forum62.

3.11.1Project 1Managing information about movies

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.

3.11.2Project 2Managing information about countries

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset