Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

JavaScript Front-End Web App Tutorial Part 1: Building a Minimal App in Seven Steps

4.87/5 (53 votes)
1 Dec 2015CPOL21 min read 358.9K   5.6K  
Learn how to build a front-end web application in plain JavaScript with minimal effort. Do not use any (third-party) framework or library, such as jQuery or Angular, which create black-box dependencies and overhead, and prevent you from learning how to do it yourself.

Introduction

This article has been extracted from the book Building Front-End Web Apps with Plain JavaScript, which is available as an open access online book. It shows how to build a front-end app with minimal effort, not using any (third-party) framework or library. While libraries and frameworks may help to increase productivity, they also create black-box dependencies and overhead, and they prevent you from learning how to do it yourself.

If you first want to see how it works and how it looks like, you can run the minimal app discussed in this article from our server.

A front-end web app can be provided by any web server, but it is executed on the user's computer device (smartphone, tablet or notebook), and not on the remote web server, as illustrated by the following architecture diagram.

Typically, but not necessarily, a front-end web app is a single-user application, which is not shared with other users.

The minimal version of a JavaScript front-end data management app discussed in this tutorial only includes a minimum of the overall functionality required for a complete app. It takes care of only one object type (Book) and supports the four standard data management operations (Create/Read/Update/Delete), but it needs to be enhanced by styling the user interface with CSS rules, and by adding further important parts of the app's overall functionality:

  • Part 2: Taking care of responsive (HTML5) constraint validation

  • Part 3: Dealing with enumerations

  • 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 a class hierarchy

Background

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

HTML

We adopt the symbolic equation

HTML = HTML5 = XHTML5

stating that when we just say "HTML", or "HTML5", we actually mean XHTML5, because we prefer the clear syntax of XML documents over the liberal and confusing HTML4-style syntax that is also allowed by HTML5.

JavaScript Objects

JavaScript objects are different from classical OO/UML objects. In particular, they need not instantiate a class. And they can have their own (instance-level) methods in the form of method slots, so they do not only have (ordinary) property slots, but also method slots. In addition, they may also have key-value slots. So, they may have three different kinds of slots, while classical objects (called "instance specifications" in UML) only have property slots.

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

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

    JavaScript
    var myRecord = { firstName:"Tom", lastName:"Smith", age:26} 
  2. A map (or 'hash map' or 'associative array') is a set of key-value slots. It supports look-ups of values based on keys like, for instance,

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

    which associates the numeric value 1 with the key "one", 2 with "two", etc. A key need not be a valid JavaScript identifier, but can be any kind of string (e.g. it may contain blank spaces).

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

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

    JavaScript
    var myApp = { model:{}, view:{}, ctrl:{} };
  5. A typed object o that instantiates a class defined by a JavaScript constructor function C is created with the expression:

    JavaScript
    var o = new C(...)

    The type/class of such a typed object can be retrieved with the introspective expression:

    JavaScript
    o.constructor.name  // returns "C" 

Maps

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

JavaScript
var i=0, key="";
for (i=0; i < Object.keys( numeral2number).length; i++) {
  key = Object.keys( numeral2number)[i];
  alert('The numeral '+ key +' denotes the number '+ numeral2number[key]);
}

For adding a new element to a map, we simply create a new key-value entry as in:

JavaScript
numeral2number["thirty two"] = 32; 

For deleting an element from a map, we can use the pre-defined JavaScript delete operator as in:

JavaScript
delete numeral2number["thirty two"];

JavaScript Supports Three Types of Data Structures

The three types of data structures (or complex datatypes) are:

  1. records, which are special JS objects, as discussed above,
  2. maps, which are also special JS objects, as discussed above,
  3. array lists, which are special JS objects called 'arrays', but since they are dynamic, they are rather array lists (as defined by Java).

Defining and Instantiating a Class

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

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

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

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

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

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

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

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

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

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

Coding the App

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 following figure.

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

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

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

  3. Update the data of a book.

  4. Delete a book record.

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

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

Step 1 - Set up the Folder Structure

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

publicLibrary
  src
    ctrl
    model
    view
  index.html

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

The minimal app's start page index.html

HTML
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Minimal JS front-end App Example</title>
  <script src="src/model/Book.js"></script>
</head>
<body>
  <h1>Public Library</h1>
  <h2>An Example of a Minimal JavaScript front-end App</h2>
  <p>This app supports the following operations:</p>
  <menu>
    <li><a href="listBooks.html"><button type="button">List all books</button></a></li>
    <li><a href="createBook.html"><button type="button">Add a new book</button></a></li>
    <li><a href="updateBook.html"><button type="button">Update a book</button></a></li>
    <li><a href="deleteBook.html"><button type="button">Delete a book</button></a></li>
    <li><button type="button" onclick="Book.clearData()">Clear database</button></li>
    <li><button type="button" onclick="Book.createTestData()">Create test data</button></li>
  </menu>
</body>
</html> 

Step 2 - Write the Model Code

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

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

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

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

  1. A class-level property Book.instances representing the collection of all Book instances managed by the application in the form of a map.

  2. A class-level method Book.loadAll 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 and storing a new Book record.

  5. A class-level method Book.update for updating an existing Book record.

  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.

1. Representing the collection of all Book instances

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

JavaScript
Book.instances = {}; 

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

Table 1: A map representing a collection of books
Key Value
006251587X { isbn:"006251587X," title:"Weaving the Web", year:2000 }
0465026567 { isbn:"0465026567," title:"Gödel, Escher, Bach", year:1999 }
0465030793 { isbn:"0465030793," title:"I Am A Strange Loop", year:2008 }

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

Table 2: A collection of book objects represented as a table
ISBN Title Year
006251587X Weaving the Web 2000
0465026567 Gödel, Escher, Bach 1999
0465030793 I Am A Strange Loop 2008

2. Loading all Book instances

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

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

    JavaScript
    bookTableString = localStorage["bookTable"];

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

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

    JavaScript
    bookTable = JSON.parse( bookTableString); 

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

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

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

Here is the full code of the procedure:

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

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

3. Saving all Book instances

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

  1. Converting the map Book.instances into a string with the help of the predefined JavaScript function JSON.stringify:

    JavaScript
    bookTableString = JSON.stringify( Book.instances);

    This conversion is called serialization.

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

    JavaScript
    localStorage["bookTable"] = bookTableString;

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

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

4. Creating a new Book instance

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

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

5. Updating an existing Book instance

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

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

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

6. Deleting an existing Book instance

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

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

7. Creating test data

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

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

8. Clearing all data

The following procedure clears all data from Local Storage:

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

Step 3 - Initialize the Application

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

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

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

Step 4 - Implement the List Objects Use Case

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

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

HTML
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Minimal JS front-end App Example</title>
  <script src="src/ctrl/initialize.js"></script>
  <script src="src/model/Book.js"></script>
  <script src="src/view/listBooks.js"></script>
  <script>
   window.addEventListener( "load", pl.view.listBooks.setupUserInterface);
  </script>
</head>
<body>
  <h1>Public Library: List all books</h1>
  <table id="books">
    <thead><tr><th>ISBN</th><th>Title</th><th>Year</th></tr></thead>
    <tbody></tbody>
  </table>
  <nav><a href="index.html">Back to main menu</a></nav>
</body>
</html>

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

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

The simple logic of this procedure consists of two steps:

  1. Read the collection of all objects from the persistent data store (in line 6).
  2. Display each object as a row in a HTML table on the screen (in the loop starting in line 9).

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

Step 5 - Implement the Create Object Use Case

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

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

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

  1. setupUserInterface takes care of 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.saveRow procedure.

JavaScript
pl.view.createBook = {
  setupUserInterface: function () {
    var saveButton = document.forms['Book'].commit;
    // load all book objects
    Book.loadAll();
    // Set an event handler for the save/submit button
    saveButton.addEventListener("click", 
        pl.view.createBook.handleSaveButtonClickEvent);
    window.addEventListener("beforeunload", function () {
        Book.saveAll(); 
    });
  },
  handleSaveButtonClickEvent: function () {
    var formEl = document.forms['Book'];
    var slots = { isbn: formEl.isbn.value, 
        title: formEl.title.value, 
        year: formEl.year.value};
    Book.add( slots);
    formEl.reset();
  }
};

Step 6 - Implement the Upate Object Use Case

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

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

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

JavaScript
pl.view.updateBook = {
  setupUserInterface: function () {
    var formEl = document.forms['Book'],
        saveButton = formEl.commit,
        selectBookEl = formEl.selectBook;
    var i=0, key="", keys=[], book=null, optionEl=null;
    // load all book objects
    Book.loadAll();
    // populate the selection list with books
    keys = Object.keys( Book.instances);
    for (i=0; i < keys.length; i++) {
      key = keys[i];
      book = Book.instances[key];
      optionEl = document.createElement("option");
      optionEl.text = book.title;
      optionEl.value = book.isbn;
      selectBookEl.add( optionEl, null);
    }
    // when a book is selected, populate the form with the book data
    selectBookEl.addEventListener("change", function () {
        var book=null, key = selectBookEl.value;
        if (key) {
          book = Book.instances[key];
          formEl.isbn.value = book.isbn;
          formEl.title.value = book.title;
          formEl.year.value = book.year;
        } else {
          formEl.isbn.value = "";
          formEl.title.value = "";
          formEl.year.value = "";
        }
    });
    saveButton.addEventListener("click", 
        pl.view.updateBook.handleUpdateButtonClickEvent);
    window.addEventListener("beforeunload", function () {
        Book.saveAll(); 
    });
  },
  // save updated data
  handleUpdateButtonClickEvent: function () {
    var formEl = document.forms['Book'];
    var slots = { isbn: formEl.isbn.value, 
        title: formEl.title.value, 
        year: formEl.year.value
    };
    Book.update( slots);
    formEl.reset();
  }
}; 

Step 7 - Implement the Delete Object Use Case

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

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

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

JavaScript
pl.view.deleteBook = {
  setupUserInterface: function () {
    var deleteButton = document.forms['Book'].commit;
    var selectEl = document.forms['Book'].selectBook;
    var i=0, key="", keys=[], book=null, optionEl=null;
    // load all book objects
    Book.loadAll();
    keys = Object.keys( Book.instances);
    // populate the selection list with books
    for (i=0; i < keys.length; i++) {
      key = keys[i];
      book = Book.instances[key];
      optionEl = document.createElement("option");
      optionEl.text = book.title;
      optionEl.value = book.isbn;
      selectEl.add( optionEl, null);
    }
    deleteButton.addEventListener("click", 
        pl.view.deleteBook.handleDeleteButtonClickEvent);
    window.addEventListener("beforeunload", function () {
        Book.saveAll(); 
    });
  },
  handleDeleteButtonClickEvent: function () {
    var selectEl = document.forms['Book'].selectBook;
    var isbn = selectEl.value;
    if (isbn) {
      Book.destroy( isbn);
      selectEl.remove( selectEl.selectedIndex);
    }
  }
};

Run the App and Get the Code

You can run the minimal app from our server, and find more resources about web engineering, including open access books, on web-engineering.info.

Possible Variations and Extensions

Using IndexDB instead of LocalStorage

Instead of using the Local Storage API, the IndexDB 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 (REST) web API one can either use a backend solution component or a cloud storage service. The remote storage approach allows managing larger databases and supports multi-user apps.

Expressing Date/Time Information with the <time> Element

Assume that our Book model class has an additional attribute publicationDate, the values of which have to be included in HTML tables and forms. While date/time information items have to be formated as strings in a human-readable form on web pages, preferably in localized form based on the settings of the user's browser, it's not a good idea to store date/time values in this form. Rather we use instances of the pre-defined JavaScript class Date for representing and storing date/time values. In this form, the pre-defined functions toISOString() and toLocaleDateString() can be used for turning Date values into ISO standard date/time strings (of the form "2015-01-27") or to localized date/time strings (like "27.1.2015"). Notice that, for simplicity, we have omitted the time part of the date/time strings.

In summary, a date/time value is expressed in three different forms:

  1. Internally, for storage and computations, as a Date value.

  2. Internally, for annotating localized date/time strings, or externally, for displaying a date/time value in a standard form, as an ISO standard date/time string, e.g., with the help of toISOString().

  3. Externally, for displaying a date/time value in a localized form, as a localized date/time string, e.g., with the help of toLocaleDateString().

When a date/time value is to be included in a web page, we can use the <time> element that allows to display a human-readable representation (typically a localized date/time string) that is annotated with a standard (machine-readable) form of the date/time value.

We illustrate the use of the <time> element with the following example of a web page that includes two <time> elements: one for displaying a fixed date, and another (initially empty) element for displaying the date of today, which is computed with the help of a JavaScript function. In both cases we use the datetime attribute for annotating the displayed human-readable date with the corresponding machine-readable representation.

HTML
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
 <meta charset="UTF-8" />
 <title>Using the HTML5 Time Element</title>
 <script src="assignDate.js"></script>
 <script>window.addEventListener("load", assignDate);</script>
</head>
<body>
 <h1>HTML5 Time Element</h1>
 <p>HTML 2.0 was published on <time datetime="1995-11-24">November 24, 1995</time>.</p>
 <p>Today is <time id="today" datetime=""></time>.</p>
</body>
</html> 

This web page loads and executes the following JavaScript function for computing today's date as a Date value and assigning its ISO standard representation and its localized representation to the <time> element:

JavaScript
function assignDate() {
  var dateEl = document.getElementById("today");
  var today = new Date();
  dateEl.textContent = today.toLocaleDateString();
  dateEl.setAttribute("datetime", today.toISOString());
}

Points of Attention

The code of this app should be extended by

  • adding some CSS styling for the user interface pages and
  • adding constraint validation.

We show how to do this in the follow-up tutorial JavaScript front-end Web Apps Tutorial Part 2: Adding Constraint Validation.

Another issue with the do-it-yourself code of this example app is the boilerplate code needed per class for the data storage management methods add, update, etc. 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 another article, Declarative and Responsive Constraint Validation with mODELcLASSjs, we present an approach how to put these methods in a generic form, such that they can be reused for all classes of an app.

Practice Projects

Feel freee to ask any questions about these projects in the CodeProject comments section below.

Project 1 - Develop a JavaScript Front-End App for Managing Movie Data

The purpose of the app is managing information about movies. Like in the book data management app discussed in the tutorial, you can make the simplifying assumption that all the data can be kept in main memory. So, on application start up, the movie data is read from a 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 in the tutorial.

The app deals with just one object type: Movie, as depicted in the Figure below. In the subsequent parts of the tutorial, you will extend this simple app by adding integrity constraints, actors and directors as further model classes, and the associations between them.

Notice that in most parts of this project you can follow, or even copy, the code of the book data management app, except that now there is an attribute with range Date, so you have to use the HTML <time> element in the list objects table, as discussed in the Section Possible Variations and Extensions” above.

For developing the app, simply follow the sequence of seven steps described in the tutorial:

  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 List Objects Use Case

  5. Step 5 - Implement the Create Object Use Case

  6. Step 6 - Implement the Upate Object Use Case

  7. Step 7 - Implement the Delete Object Use Case

Project 2 - Managing Persistent Data with IndexedDB

Improve your app developed in project 1 by replacing the use of the Local Storage API for persistent data storage with using the more powerful IndexedDB API.

History

  • 2 April 2014: First version created
  • 3 April 2014: Corrected typos, added link, added new subsection "HTML" and new section "Points of Variation"
  • 11 April 2014: Improved table formating, updated code, resolved mismatches between article and code
  • 12 May 2014: Updated hyperlinks
  • 6 October 2014: Updated hyperlinks, upload a diagram image, added final section "Points of Attention"
  • 7 January 2015: Added the section "JavaScript Supports Three Types of Data Structures", corrected hyperlinks.
  • 28 January 2015: Extended the section "Points of Variation" (and changed title to "Possible Variations and Extensions") by adding two subsections: 1) Using IndexDB instead of LocalStorage, and 2) Expressing Date/Time Information with the <time> Element. Improved teaser.
  • 15 February 2015: Refactored the wrong declaration of counter variables in for loops, like in (var i=0;...)
  • 17 June 2015: Added new Section "Practice Projects"
  • 1 December 2015: Added front-end app architecture diagram

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)