2. Step 2 - Write 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:

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

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 avoid using the global scope (either with the help of namespace objects or by using ES6 modules).

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.

2.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:

Book.instances = {};

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.

2.2. 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:

Book.add = function (slots) {
  const book = new Book( slots);
  // add book to the collection of Book.instances 
  Book.instances[slots.isbn] = book;
  console.log(`Book ${slots.isbn} created!`);
};

2.3. Retrieving all Book instances

For persistent data storage, we use the Local Storage API supported by modern web browsers. Retrieving 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

    booksString = localStorage["books"];
  2. Converting the book table string into a corresponding entity table books with book rows as elements, with the help of the built-in function JSON.parse:

    books = JSON.parse( booksString);

    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:

    Book.convertRec2Obj = function (bookRow) {
      const book = new Book( bookRow);
      return book;
    };

Here is the full code of the procedure:

Book.retrieveAll = function () {
  var booksString="";  
  try {
    if (localStorage["books"]) {
      booksString = localStorage["books"];
    }
  } catch (e) {
    alert("Error when reading from Local Storage\n" + e);
  }
  if (booksString) {
    const books = JSON.parse( booksString);
    const keys = Object.keys( books);
    console.log(`${keys.length} books loaded.`);
    for (const key of keys) {
      Book.instances[key] = Book.convertRec2Obj( books[key]);
    }
  }
};

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.

2.4. Updating 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:

Book.update = function (slots) {
  const book = Book.instances[slots.isbn],
        year = parseInt( slots.year);  // convert string to integer
  if (book.title !== slots.title) book.title = slots.title;
  if (book.year !== year) book.year = year;
  console.log(`Book ${slots.isbn} modified!`);
};

2.5. Deleting 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:

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!`);
  }
};

2.6. 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 entity table Book.instances into a string with the help of the predefined JavaScript procedure JSON.stringify:

    booksString = JSON.stringify( Book.instances);

    This conversion is called serialization.

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

    localStorage["books"] = booksString;

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

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

2.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:

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();
};

2.8. Clearing all data

The following procedure clears all data from Local Storage:

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