7. The View and Controller Layers

The user interface (UI) consists of a start page index.html that allows the user choosing one of the data management operations by navigating to the corresponding UI page such as listBooks.html or createBook.html in the app folder. The start page index.html has been discussed in Section 5.3.

After loading the Pure base stylesheet and our own CSS settings in main.css, we first load some browser shims and utility functions. Then we initialize the app in src/ctrl/initialize.js and continue loading the error classes defined in lib/errorTypes.js and the model class Book.

We render the data management menu items in the form of buttons. For simplicity, we invoke the Book.clearData() and Book.createTestData() methods directly from the buttons' onclick event handler attribute. Notice, however, that it is generally preferable to register such event handling functions with addEventListener(...), as we do in all other cases.

7.1. The data management UI pages

Each data management UI page loads the same basic CSS and JavaScript files like the start page index.html discussed above. In addition, it loads two use-case-specific view and controller files src/view/useCase.js and src/ctrl/useCase.js and then adds a use case initialize function (such as pl.ctrl.listBooks.initialize) as an event listener for the page load event, which takes care of initializing the use case when the UI page has been loaded (see Section 7.3).

For the "list books" use case, we get the following code in listBooks.html:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>JS Front-End Validation App Example</title>
  <link rel="stylesheet" 
        href="http://yui.yahooapis.com/pure/0.3.0/pure-min.css" />
  <link rel="stylesheet" href="css/main.css" /> 
  <script src="lib/browserShims.js"></script>
  <script src="lib/util.js"></script>
  <script src="lib/errorTypes.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><th>Edition</th></tr>
    </thead>
    <tbody></tbody>
  </table>
  <nav><a href="index.html">Back to main menu</a></nav>
</body>
</html>

For the "create book" use case, we get the following code in createBook.html:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta charset="UTF-8" />
  <title>JS Front-End Validation App Example</title>
  <link rel="stylesheet" 
        href="http://yui.yahooapis.com/combo?pure/0.3.0/base-
              min.css&pure/0.3.0/forms-min.css" />
  <link rel="stylesheet" href="css/main.css" /> 
  <script src="lib/browserShims.js"></script>
  <script src="lib/util.js"></script>
  <script src="lib/errorTypes.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" class="pure-form pure-form-aligned">
    <div class="pure-control-group">
      <label for="isbn">ISBN</label>
      <input id="isbn" name="isbn" />
    </div>
    <div class="pure-control-group">
      <label for="title">Title</label>
      <input id="title" name="title" />
    </div>
    <div class="pure-control-group">
      <label for="year">Year</label>
      <input id="year" name="year" />
    </div>
    <div class="pure-control-group">
      <label for="edition">Edition</label>
      <input id="edition" name="edition" />
    </div>
    <div class="pure-controls">
      <p><button type="submit" name="commit">Save</button></p>
      <nav><a href="index.html">Back to main menu</a></nav>
    </div>
  </form>
</body>
</html>

Notice that for styling the form elements in createBook.html, and also for updateBook.html and deleteBook.html, we use the Pure CSS form styles. This requires to assign specific values, such as "pure-control-group", to the class attributes of the form's div elements containing the form controls. We have to use explicit labeling (with the label element's for attribute referencing the input element's id), since Pure does not support implicit labels where the label element contains the input element.

7.2. Initialize the app

For initializing the app, its namespace and MVC subnamespaces have to be defined. For our example app, the main namespace is defined to be pl, standing for "Public Library", with the three subnamespaces model, view and ctrl being initially empty objects:

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

We put this code in the file initialize.js in the ctrl folder.

7.3. Initialize the data management use cases

For initializing a data management use case, the required data has to be loaded from persistent storage and the UI has to be set up. This is performed with the help of the controller procedures pl.ctrl.createBook.initialize and pl.ctrl.createBook.loadData defined in the controller file ctrl/createBook.js with the following code:

pl.ctrl.createBook = {
  initialize: function () {
    pl.ctrl.createBook.loadData();
    pl.view.createBook.setupUserInterface();
  },
  loadData: function () {
    Book.loadAll();
  }
};

All other data management use cases (read/list, update, delete) are handled in the same way.

7.4. Set up the user interface

For setting up the user interfaces of the data management use cases, we have to distinguish the case of "list books" from the other ones (create, update, delete). While the latter ones require using an HTML form and attaching event handlers to form controls, in the case of "list books" we only have to render a table displaying all the books, as shown in the following program listing of view/listBooks.js:

 pl.view.listBooks = {
  setupUserInterface: function () {
    var tableBodyEl = document.querySelector("table#books>tbody");
    var i=0, book=null, row={}, key="", 
        keys = Object.keys( Book.instances);
    for (i=0; i < keys.length; i++) {
      key = keys[i];
      book = Book.instances[key];
      row = tableBodyEl.insertRow(-1);
      row.insertCell(-1).textContent = book.isbn;
      row.insertCell(-1).textContent = book.title;
      row.insertCell(-1).textContent = book.year;
      if (book.edition) {
        row.insertCell(-1).textContent = book.edition;
      }
    }
  }
};

For the create, update and delete use cases, we need to attach the following event handlers to form controls:

  1. a function, such as handleSubmitButtonClickEvent, for handling the event when the user clicks the save/submit button,

  2. functions for validating the data entered by the user in form fields (if there are any).

In addition, in line 28 of the following view/createBook.js code, we add an event handler for saving the application data in the case of a beforeunload event, which occurs, for instance, when the browser (or browser tab) is closed:

pl.view.createBook = {
  setupUserInterface: function () {
    var formEl = document.forms['Book'],
        submitButton = formEl.commit;
    submitButton.addEventListener("click", 
        this.handleSubmitButtonClickEvent);
    formEl.isbn.addEventListener("input", function () { 
        formEl.isbn.setCustomValidity( 
            Book.checkIsbnAsId( formEl.isbn.value).message);
    });
    formEl.title.addEventListener("input", function () { 
        formEl.title.setCustomValidity( 
            Book.checkTitle( formEl.title.value).message);
    });
    formEl.year.addEventListener("input", function () { 
        formEl.year.setCustomValidity( 
            Book.checkYear( formEl.year.value).message);
    });
    formEl.edition.addEventListener("input", function () { 
        formEl.edition.setCustomValidity( 
            Book.checkEdition( formEl.edition.value).message);
    });
    // neutralize the submit event
    formEl.addEventListener( 'submit', function (e) { 
        e.preventDefault();;
        formEl.reset();
    });
    window.addEventListener("beforeunload", function () {
        Book.saveAll(); 
    });
  },
  handleSubmitButtonClickEvent: function () {
    ...
  }
};

Notice that for each form input field we add a listener for input events, such that on any user input a validation check is performed because input events are created by user input actions such as typing. We use the predefined function setCustomValidity from the HTML5 form validation API for having our property check functions invoked on the current value of the form field and returning an error message in the case of a constraint violation. So, whenever the string represented by the expression Book.checkIsbn( formEl.isbn.value).message is empty, everything is fine. Otherwise, if it represents an error message, the browser indicates the constraint violation to the user by rendering a red outline for the form field concerned (due to our CSS rule for the :invalid pseudo class).

While the validation on user input enhances the usability of the UI by providing immediate feedback to the user, validation on form data submission is even more important for catching invalid data. Therefore, the event handler handleSubmitButtonClickEvent() performs the property checks again with the help of setCustomValidity, as shown in the following program listing:

handleSubmitButtonClickEvent: function () {
  var formEl = document.forms['Book'];
  var slots = { isbn: formEl.isbn.value, 
          title: formEl.title.value,
          year: formEl.year.value,
          edition: formEl.edition.value
  };
  // set error messages in case of constraint violations
  formEl.isbn.setCustomValidity( Book.checkIsbnAsId( slots.isbn).message);
  formEl.title.setCustomValidity( Book.checkTitle( slots.title).message);
  formEl.year.setCustomValidity( Book.checkYear( slots.year).message);
  formEl.edition.setCustomValidity( 
      Book.checkEdition( formEl.edition.value).message);
  // save the input data only if all of the form fields are valid
  if (formEl.checkValidity()) {
    Book.add( slots);
  }
}

By invoking checkValidity() on the form element, we make sure that the form data is only saved (by Book.add), if there is no constraint violation. After this handleSubmitButtonClickEvent handler has been executed on an invalid form, the browser takes control and tests if the pre-defined property validity has an error flag for any form field. In our approach, since we use setCustomValidity, the validity.customError would be true. If this is the case, the custom constraint violation message will be displayed (in a bubble) and the submit event will be suppressed.

For the use case update book, which is handled in view/updateBook.js, we provide a book selection list, so the user need not enter an identifier for books (an ISBN), but has to select the book to be updated. This implies that there is no need to validate the ISBN form field, but only the title and year fields. We get the following code:

pl.view.updateBook = {
  setupUserInterface: function () {
    var formEl = document.forms['Book'],
        submitButton = formEl.commit,
        selectBookEl = formEl.selectBook;
    // set up the book selection list
    util.fillSelectWithOptions( Book.instances, 
	    selectBookEl, "isbn", "title");
    // when a book is selected, populate the form with its data
    selectBookEl.addEventListener("change", function () {
      var book=null, bookKey = selectBookEl.value;
      if (bookKey) {  // set form fields and reset CustomValidity
        book = Book.instances[bookKey];
        ["isbn","title","year","edition"].forEach( function (p) {
          formEl[p].value = book[p] !== undefined ? book[p] : "";
          formEl[p].setCustomValidity("");  // no error
        });
      } else {
        formEl.reset();
      }
    });
    ...  // add event listeners
  },

We also need to set up a number of event listeners, including input event listeners for responsive validation:

pl.view.updateBook = {
  setupUserInterface: function () {
    ...
    formEl.title.addEventListener("input", function () { 
        formEl.title.setCustomValidity( 
            Book.checkTitle( formEl.title.value).message);
    });
    formEl.year.addEventListener("input", function () { 
        formEl.year.setCustomValidity( 
            Book.checkYear( formEl.year.value).message);
    });
    formEl.edition.addEventListener("input", function () { 
        formEl.edition.setCustomValidity( 
            Book.checkEdition( formEl.edition.value).message);
    });
    submitButton.addEventListener("click", 
        this.handleSubmitButtonClickEvent);
    // neutralize the submit event
    formEl.addEventListener( 'submit', function (e) { 
        e.preventDefault();;
        formEl.reset();
    });
    window.addEventListener("beforeunload", function () {
        Book.saveAll(); 
    });
  },

When the save button on the update book form is clicked, the title and year form field values are validated by invoking setCustomValidity, and then the book record is updated if the form data validity can be established with checkValidity():

  handleSubmitButtonClickEvent: function () {
    var formEl = document.forms['Book'];
    var slots = { isbn: formEl.isbn.value, 
        title: formEl.title.value,
        year: formEl.year.value,
        edition: formEl.edition.value 
    };
    // set error messages in case of constraint violations
    formEl.title.setCustomValidity( 
        Book.checkTitle( slots.title).message);
    formEl.year.setCustomValidity( 
        Book.checkYear( slots.year).message);
    formEl.edition.setCustomValidity( 
            Book.checkEdition( formEl.edition.value).message);
    if (formEl.checkValidity()) {
      Book.update( slots);
    }
  }

The logic of the setupUserInterface method for the delete use case is similar.