BackboneJS Frontend Web App with Cloud Storage Tutorial Part 1: Building a Minimal ...

:

Introduction

In this tutorial, which has been extracted from the open access book Engineering Front-End Web Apps with BackboneJS and Cloud Storage, we show how to build a minimal front-end web application with the BackboneJS framework and the cloud storage service Parse.com. Most parts of the app's code are executed on the front-end, while the persistent storage functionality is executed on the back-end server(s) in the cloud infrastructure managed by Parse.com. If you want to see how it works, you can run the minimal app discussed in this article from our server.

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.

Background

BackboneJS is one of the most popular JavaScript frameworks for building front-end web applications.

Parse.com is a cloud service platform providing a remote storage service that can be invoked from a JavaScript front-end program based on an adapted version of the BackboneJS library. More specifically, for using the storage service we need three BackboneJS/Parse API classes:

  1. Parse.Object allows to define model classes for creating objects that can be saved to, and retrievd from, the Parse data store;

  2. Parse.Query allows to query the Parse data store;

  3. Parse.Collection allows to represent and process collections of objects, such as the population of a table or the result set of a query.

For maintaining a collection of data objects in a persistent data store, the Parse platform provides back-end storage services, that can be invoked via a web API from BackboneJS model objects with methods such as save and destroy using the JavaScript XML HTTP Request (XHR) API for exchanging HTTP messages with the Parse server. In general, sending data to a remote back-end server from a JavaScript front-end program is done with asynchronous remote procedure calls using XHR. Since such a remote procedure call can result either in a reply message confirming the successful completion of the call or indicating an error state, we generally need to specify two JavaScript methods as the arguments for calling the remote procedure asynchronously: the first method is invoked when the remote procedure call has been successfully completed, and the second one when it failed.

Using the code

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.

https://www.codeproject.com/KB/scripting/753724/Book.png

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.

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 can 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, either locally or remotely. An attractive option for remote storage is using a cloud storage service, where data is stored in logical data pools, which may physically span multiple servers in a cloud infrastructure owned and managed by a cloud service company. A cloud storage service is typically reachable via a web API, which may be integrated with the data management methods of a framework, as in the case of the cloud storage service provided by Parse.com. For our minimal BackboneJS app, we will therefore use the adapted version of BackboneJS provided by Parse.com.

Getting Started with Parse

Any web page of your BackboneJS/Parse app needs to load the BackboneJS/Parse library from the Parse content delivery network with the help of the following HTML script element:

<script src="http://www.parsecdn.com/js/parse-1.3.5.min.js"></script>

For being able to use a Parse service via a BackboneJS/Parse method invocation in your app's JavaScript code, you need to set up a user account on the Parse website. Then, go to the Dashboard and create a new Parse app account using the name of your app. After your Parse app account has been created, you can look up its Application ID and its JavaScript Key under "Application Keys". You need to provide these two keys as the arguments of the Parse.initialize method when you initialize your app, as in the following code pattern:

Parse.initialize("APPLICATION_ID", "JAVASCRIPT_KEY");

The BackboneJS/Parse Model Layer

 Defining a model class

A BackboneJS/Parse model class is defined by extending the predefined class Parse.Object using the function Parse.Object.extend, providing the name of the new class and certain class definition slots. In the simplest case, such a definition only includes a defaults slot for defining the class attributes' default values. This is shown in the following definition of a model class Book, which serves as our running example:

var Book = Parse.Object.extend("Book", {
  defaults: {
    isbn: "",
    title: "",
    year: 0
  }
});

Notice that the defaults slot sort of defines the properties of a class. We create a new object as an instance of a model class in the following way:

var book = new Book({
      isbn: "123456789X", 
      title: "BackboneJS/Parse Tutorial", 
      year: 2013
});

After the object has been created, its property slots are not represented as direct slots (e.g., there is no book.isbn slot), but rather as key-value slots in the predefined map-valued property attributes

We can access the value of a property with the help of the method get as in:

var publicationYear = book.get("year");

We can assign a value to a property with the help of the method set as in:

book.set("year", 2014);

Representing the collection of all instances of a class

For representing the collection of all the instances of a class C managed by the application, we define the class-level property C.instances. This property is set to the Parse collection representing the population of the class in the following way:

Book.instances = (new Parse.Query( Book)).collection();

The class-level property Book.instances is set to the Parse collection that stores the results from the query retrieving the entire population of the Book table with the help of the query object new Parse.Query( Book).

Fetching data from the Parse data store

For initializing a data management use case, we generally need to fetch the current data tfrom the Parse data store. Since Book.instances is a Parse collection, we can apply the fetch method to it for refreshing its contens from the current state of the Parse data store:

Book.instances.fetch({  // load data from Parse data store
  success: function (coll) {
    ...;  // set up the user interface
  },
  error: function (coll, error) {
    console.log("Error: " + error);
  }
});

Writing data to the Parse data store

Writing data to the Parse data store is done with the help of the Parse method save, as shown in the following example

var book = new Book( ...);
book.save( null, {
  success: function (obj) {
    console.log("Saved: " + JSON.stringify(obj));
  },
  error: function (obj, error) {
    console.log("Error: " + error + " for " + JSON.stringify(obj));
  }
});

Notice that, as an asynchronous remote procedure call, the Parse save method is invoked with a success and an error callback method as arguments.

To make sure the data was saved, you can look it up with the web user interface of Parse. You should see something like in the following screen shot.

Three attributes are provided by Parse automatically. The attribute objectId is a unique identifier for each Parse object, while createdAt and updatedAt represent the time that each object was created and last modified in the Parse data store. Each of these attributes is filled in by Parse, so they are read-only in your app's JavaScript code.

Destroying a Parse object

Deleting an existing object from the Parse data store is done with the help of the Parse method destroy:

var book = Book.instances.get( id);
book.destroy({
  success: function (obj) {
    console.log("Deleted book "+ JSON.stringify(obj));
  },
  error: function (obj, error) {
    console.log( error + "\nCannot delete the book " + 
        JSON.stringify(obj) +". Not found in database!");
  }
});

Building a Minimal BackboneJS/Parse App in Seven Steps

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 add three subfolders: "css" for our CSS style files, "lib" for the libraries used in the app, and "src" for our JavaScript source code files. In the "src" folder, we create the subfolders "model", "view" and "ctrl", following the Model-View-Controller application architecture paradigm. Thus, we get the following folder structure:

publicLibrary
  css
  lib
  src
    ctrl
    model
    view

Step 2 - Write the Model Code

In the second step, we create the model classes for our app, using one JavaScript file for each model class. In the information model for our example app, shown in Figure Figure 2.1, there is only one class, representing the object type Book. So, in the folder src/model, we create a file Book.js containig the entire code for the model class Book.

The first part of this JavaScript contains the Parse/Backbone model class definition, which for now only includes a defaults property for defining the attributes' default values:

var Book = Parse.Object.extend("Book", {
  defaults: {
    isbn: "",
    title: "",
    year: 0
  }
});

 

In addition to defining the Parse/Backbone model class, 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.

  2. A class-level method Book.add for creating a new book record in the Parse data store.

  3. A class-level method Book.update for updating a book record in the Parse data store.

  4. A class-level method Book.destroy for deleting a book record from the Parse data store.

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:

Book.instances = (new Parse.Query( Book)).collection();

The class-level property Book.instances is set to the Parse collection object obtained by converting the result list from retrieving the population of the Book table with the help of the query object new Parse.Query( Book).

Creating a new Book record

Creating and storing a new book record in the Parse data store is done with the help of the following class-level method:

Book.add = function (slots) {
  var book = null;
  // make sure that year is an integer
  slots.year = parseInt( slots.year);
  book = new Book( slots);
  book.save( null, {
    success: function (obj) {
      console.log("Saved: "+ JSON.stringify(obj));
    },
    error: function (obj, error) {
      console.log("Error: "+ error + " for "+ JSON.stringify(obj));
    }
  });
};

Updating an existing Book record

An existing book record can be updated in the Parse data store witrh the help of the following class-level method:

Book.update = function (id, slots) {
  var book = Book.instances.get( id);
  // make sure that year is an integer
  slots.year = parseInt( slots.year);  
  if (book.title !== slots.title) { 
    book.set("title", slots.title);
  }
  if (book.year !== slots.year) { 
    book.set("year", slots.year);
  }
  book.save( null, {
    success: function (obj) {
      console.log("Saved: "+ JSON.stringify(obj));
    },
    error: function (obj, error) {
      console.log("Error: "+ error + "Model: "+ JSON.stringify(obj));
    }
  });
};

Deleting an existing Book record

Deleting an existing book record in the Parse data store is done witrh the help of the following class-level method:

Book.destroy = function (id) {
  var book = Book.instances.get( id);
  Book.instances.remove( book);
  book.destroy({
    success: function (obj) {
      console.log("Deleted book "+ JSON.stringify(obj));
    },
    error: function (obj, error) {
      console.log( error + "\nCannot delete the book " + 
          JSON.stringify(obj) +". Not found in database!");
    }
  });
};

The following procedure creates 3 book objects and saves them in the Parse data store using Parse.Object.saveAll:

Book.createTestData = function () {
  var book1 = new Book(
        {isbn:"006251587X", title:"Weaving the Web", year:2000});
  var book2 = new Book(
        {isbn:"0465026567", title:"Gödel, Escher, Bach", year:1999});
  var book3 = new Book(
        {isbn:"0465030793", title:"I Am A Strange Loop", year:2008});
  var list = [book1, book2, book3];
  Parse.Object.saveAll( list, {
    success: function (list) {
      console.log("Saved: "+ list.length);
    },
    error: function (error) {
      console.log("Error: "+ error);
    }
  });
};

The Parse user interface allows you to take a look at your Parse tables, and you should see a table like the following, but with additional Parse attributes.

Table 1: A collection of book objects represented as a Parse table

ISBN Title Year
006251587X Weaving the Web 2000
0465026567 Gödel, Escher, Bach 1999
0465030793 I Am A Strange Loop 2008

Step 3 - Initialize the App

We initialize the application in src/ctrl/initialize.js by defining its namespaces and by invoking the Parse.initialize procedure:

var pl = { model:{}, view:{}, ctrl:{} };
Parse.initialize("APPLICATION_ID", "JAVASCRIPT_KEY");

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.

You have to replace the srtings APPLICATION_ID with your Parse Application ID, and JAVASCRIPT_KEY with your Parse JavaScript Key, which you can look up on your Parse account page.

Step 4 - Develop the Use Case "List Objects"

This use case represents the "Read" from the four basic data management use cases Create-Read-Update-Delete (CRUD). The simple logic of this use case consists of two steps:

  1. Read the collection of all objects from the persistent data store.

  2. Display each object as a row in a HTML table on the screen.

The user interface for this use case is provided by the HTML file listBooks.html (in the main folder publicLibrary), consisting of the following code:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Minimal Parse/Backbone App Example</title>
  <script src="http://www.parsecdn.com/js/parse-1.3.5.min.js"></script>
  <script src="src/ctrl/initialize.js"></script>
  <script src="src/model/Book.js"></script>
  <script src="src/view/listBooks.js"></script>
  <script src="src/ctrl/listBooks.js"></script>
  <script>
    window.addEventListener("load", pl.ctrl.listBooks.initialize);
  </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>
</body>
</html>

Notice that this HTML file, which is used for running the "list books" use case, loads four JavaScript files: the Parse function library parse-1.3.5.min.js from the Parse website, the application initialization code file src/ctrl/initialize.js, the model code file src/model/Book.js, the view code file src/view/listBooks.js and the controller code file src/ctrl/listBooks.js. While the model code file and the application initialization code file have been discussed above, we now develop the "list books" use case's controller and view code files.

For initializing this use case, we have the following code in src/ctrl/listBooks.js:

pl.ctrl.listBooks = {
  initialize: function () {
    Book.instances.fetch({  // load data from Parse data store
      success: function (coll) {
        pl.view.listBooks.setUpUserInterface(); 
      },
      error: function (coll, error) {
        console.log("Error: " + error);
      }
    });
  }
};

Notice that the initialize procedure invokes the setUpUserInterface procedure when the instances of the Book model class can be fetched from the Parse data store (in the success callback).

The setUpUserInterface procedure sets up the user interface by constructing a HTML table populated with the book records from the Book.instances collection:

pl.view.listBooks = {
  setUpUserInterface: function () {
    var tableBodyEl = document.querySelector("table#books>tbody");
    Book.instances.forEach( function (bkObj) {
      var row = tableBodyEl.insertRow(-1);
      row.insertCell(-1).textContent = bkObj.get("isbn");      
      row.insertCell(-1).textContent = bkObj.get("title");  
      row.insertCell(-1).textContent = bkObj.get("year");
    });
  }
};

The view table is created in a loop over all elements of the Parse collection Book.instances. In each step of this loop, a new HTML table row is created in the table body element with the help of the DOM operation insertRow, and then three cells are created within 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, which are accessed using the get method on the book Parse object. 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 - Develop the Use Case "Create Object"

The HTML user interface for the use case "create book" requires a form with a form field for each attribute of the Book model class. For our example app, this page would be called createBook.html (in the main folder publicLibrary) and would contain the following HTML code:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Minimal Parse/Backbone App Example</title>
    <script src="http://www.parsecdn.com/js/parse-1.3.5.min.js"></script>
    <script src="src/ctrl/initialize.js"></script>
    <script src="src/model/Book.js"></script>
    <script src="src/view/createBook.js"></script>
    <script src="src/ctrl/createBook.js"></script>
    <script>
      window.addEventListener("load", pl.ctrl.createBook.initialize);
    </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>
    <p><a href="index.html">Main menu</a></p>
  </body>
</html>

As in all four use cases, we load both a use-case-specific controller code file, src/ctrl/createBook.js, and a view code file:, src/view/createBook.js, which are presented in the next two program listings.

The controller code file just contains an initialize procedure for initializing the use case in the same way as for the "list books" use case, except that after succesfully fetching all book records from the Parse data store, the procedure for settiing up the "create book" user interface is invoked:

pl.ctrl.createBook = {
  initialize: function () {    
    Book.instances.fetch({  // load data from Parse data store
      success: function (coll) {
        pl.view.createBook.setUpUserInterface(); 
      },
      error: function (coll, error) {
        console.log("Error: " + error);
      }
    });
  }
};

The view code file just contains the setUpUserInterface procedure for setting up the HTML user interface of the use case:

pl.view.createBook = {
  setUpUserInterface: function () {
    var saveButton = document.forms['Book'].commit;
    saveButton.addEventListener("click", function (e) {
      var formEl = document.forms['Book'];
      var slots = { isbn: formEl.isbn.value, 
            title: formEl.title.value, 
            year: formEl.year.value
          };
      Book.add( slots);
      formEl.reset();
    });
  }
};

In the event handler for clicking the save button, the form field values corresponding to the Book attributes isbn, title and year are retrieved and passed to the method Book.add for creating and storing a new book record. Finally, the "create book" form is reset

Step 6 - Develop the Use Case "Update Object"

In the form for the "update book" user interface (publicLibrary/updateBook.html), the form field for the standard identifier attribute isbn is an HTML output element because the user is not allowed to change the standard identifier of an object in the "update object" use case. Otherwise, the form is very similar to createBook.html. It contains the following HTML code:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Minimal Parse/Backbone App Example</title>
    <script src="http://www.parsecdn.com/js/parse-1.3.5.min.js"></script>
    <script src="src/ctrl/initialize.js"></script>
    <script src="src/model/Book.js"></script>
    <script src="src/view/updateBook.js"></script>
    <script src="src/ctrl/updateBook.js"></script>
    <script>
      window.addEventListener("load", pl.ctrl.updateBook.initialize);
    </script>
  </head>
  <body>
    <h1>Public Library: Update a book record</h1>
    <form id="Book">
      <p><label>Select book: 
        <select name="selectBook"><option value="0">-----</option></select>
      </label></p>
      <p><label>ISBN: <output 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 Changes</button></p>
    </form>
    <p><a href="index.html">Main menu</a></p>
  </body>
</html>

The controller code file just contains an initialize procedure for initializing the use case in the same way as before:

pl.ctrl.updateBook = {
  initialize: function () {
    // load book data from Parse DB table
    Book.instances.fetch({
      success: function (coll) {
        pl.view.updateBook.setUpUserInterface(); 
      },
      error: function (coll, error) {
        console.log("Error: " + error);
      }
    });
  }
};

The view code file just contains the setUpUserInterface procedure for setting up the HTML user interface of the use case:

pl.view.updateBook = {
  setUpUserInterface: function () {
    var formEl = document.forms['Book'],
        saveButton = formEl.commit,
        selectBookEl = formEl.selectBook;
    // populate the selection list
    Book.instances.forEach( function (book) {
      var bookOptionEl = document.createElement("option");
      bookOptionEl.text = book.get("title");
      bookOptionEl.value = book.id;  // the Parse object ID
      selectBookEl.add( bookOptionEl, null);
    });
    // when a book is selected, fill the form with its data
    selectBookEl.addEventListener("change", function () {
      var book = null, 
          bookObjId = selectBookEl.value;
      if (bookObjId !== "0") {
        book = Book.instances.get( bookObjId);
        formEl.isbn.value = book.get("isbn");
        formEl.title.value = book.get("title");
        formEl.year.value = book.get("year");
      } else {  // no book selected
        formEl.isbn.value = "";
        formEl.title.value = "";
        formEl.year.value = "";
      }
    });
    //  event handler for clicking the save button
    saveButton.addEventListener("click", function () {
      var formEl = document.forms['Book'];
      var bookObjId = formEl.selectBook.value;
      var slots = { isbn: formEl.isbn.value, 
            title: formEl.title.value, 
            year: formEl.year.value
          };
      Book.update( bookObjId, slots);
      formEl.reset();
    });
  }
};

This procedure has three parts:

  1. The book selection list is populated with the titles of the books that are already stored, while the value of each selection option is set to the book's Parse object ID.

  2. In a change event handler for the book selection field, we populate the form with the data of the selected book whenever a book is selected.

  3. In the event handler for clicking the update button we invoke the method Book.update for updating the selected book record, and then reset the "update book" form.

Step 7 - Develop the Use Case "Delete Object"

In the form for the "delete book" user interface (publicLibrary/deleteBook.html), we just need a book selection field, as in updateBook.html:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Minimal Parse/Backbone App Example</title>
    <script src="http://www.parsecdn.com/js/parse-1.2.8.min.js"></script>
    <script src="src/ctrl/initialize.js"></script>
    <script src="src/model/Book.js"></script>
    <script src="src/view/deleteBook.js"></script>
    <script src="src/ctrl/deleteBook.js"></script>
    <script>
      window.addEventListener("load", pl.ctrl.deleteBook.initialize);
    </script>
  </head>
  <body>
    <h1>Public Library: Delete a book record</h1>
    <form id="Book">
      <p>
        <label>Select book: <select name="selectBook"></select></label>
      </p>
      <p><button type="button" name="commit">Delete</button></p>
    </form>
    <p><a href="index.html">Main menu</a></p>
  </body>
</html>

The controller code file again just contains an initialize procedure for initializing the use case by fetching all book records from the Parse data store and invoking the setUpUserInterface procedure:

pl.ctrl.deleteBook = {
  initialize: function () {
    // load book data from Parse DB table
    Book.instances.fetch({
      success: function (coll) {
        pl.view.deleteBook.setUpUserInterface(); 
      },
      error: function (coll, error) {
        console.log("Error: " + error);
      }
    });
  }
};

The view code file just contains the setUpUserInterface procedure for setting up the HTML user interface of the use case:

pl.view.deleteBook = {
  setUpUserInterface: function () {
    var deleteButton = document.forms['Book'].commit;
    var selectEl = document.forms['Book'].selectBook;
    // populate the select list with books
    Book.instances.forEach( function (book) {
      var bookOptionEl = document.createElement("option");
      bookOptionEl.text = book.get("title");
      bookOptionEl.value = book.id;  // the Parse object ID
      selectEl.add( bookOptionEl, null);
    });
    deleteButton.addEventListener("click", function () {
      var selectEl = document.forms['Book'].selectBook,
          id = selectEl.value;
      if (id) {
        Book.destroy( id);
        selectEl.remove( selectEl.selectedIndex);
      }
    });
  }
};

This procedure has two parts:

  1. The book selection list is populated with the titles of the books that are already stored, while the value of each selection option is set to the book's Parse object ID.

  2. In the event handler for clicking the delete button we invoke the method Book.destroy for deleting the selected book record, and then also remove the book from the options of the selection list.

3. Run the App and Get the Code

You can run the minimal app from our server or download the code as a ZIP archive file from from CodeProject (see the download link above). Recall that you have to edit the src/ctrl/initialize.js file and enter your Parse APPLICATION_ID and JAVASCRIPT_KEY before you run it, as discussed above.

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 Adding Constraint Validation.

Notice that in this tutorial, we have made the assumption that all application data can be loaded into main memory (like all book data is loaded into the collection Book.instances). In the case of remote storage, this approach only works for very small databases. Otherwise, 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 class for the data storage management methods add, update, and destroy. While it is good to write this code a few times for learning app development, you don't want to write it again and again later when you work on real projects. In another tutorial, we present an approach how to put these methods in a generic form in a meta-class called mODELcLASS, such that they can be reused in all model classes of an app.

History

  • March 11, 2015, first version publsihed.