7. Write the View Code

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 retrieveAndListAllBooks.html or createBook.html.

The start page index.html has been discussed above in Section 5.2. It sets up two buttons for clearing the app's database by invoking the procedure Book.clearData() and for creating sample data by invoking the procedure Book.createTestData() from the buttons' click event listeners.

Each data management UI page useCase.html loads the same basic CSS and JavaScript files like the start page index.html discussed above. In addition, it loads a use-case-specific view module file src/v/useCase.mjs. The CSS file main.css now contains the following rule for marking invalid form fields by drawing a red outline:

form:invalid {
    outline: dotted red;
} 

For setting up the user interfaces of the data management use cases, we have to distinguish the case of "Retrieve/List All" 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 "Retrieve/List All" we only have to render a table displaying all books, as in the case of the Minimal App discussed in Chapter 3.

For all four CRUD user interfaces, in the use-case-specific view module file src/v/useCase.mjs, we need three code blocks for

  1. importing the Book class and possibly other items,

  2. loading data (in particular, all book records), and

  3. defining variables for accessing various UI elements.

These three steps may look like so:

// import classes and other items
import Book from "../m/Book.mjs";
// load data
Book.retrieveAll();
// define variables for accessing UI elements
const formEl = document.forms["Book"],
      saveButton = formEl["commit"];

In addition, in the three cases of Create, Update and Delete, we have to add several code blocks for defining event listeners for:

  1. responsive validation on form field input events,

  2. handling the event when the user pushes the save (or delete) button,

  3. making sure the main memory data is saved when a beforeunload event occurs, that is, when the browser window/tab is closed.

7.1. Set up the user interface for Create Book

For the use case Create, we obtain the following code (in v/createBook.mjs) for adding event listeners for responsive validation:

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

Notice that for each 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).

In addition to the event handlers for responsive constraint validation, we need two more event handlers: one for validation on form data submission and one for the event when the browser window (or tab) is closed.

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. In the form data submission event handler, the property checks are performed again (with the help of setCustomValidity), as shown in the following program listing:

saveButton.addEventListener("click", function () {
  const slots = { isbn: formEl.isbn.value,
          title: formEl.title.value,
          year: formEl.year.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);
  if (formEl.edition.value) {
    slots.edition = formEl.edition.value;
    formEl.edition.setCustomValidity( 
        Book.checkEdition( slots.edition).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 event handler has been executed on an invalid form, the browser takes control and tests if the predefined 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).

Since the Save button has the type "submit", clicking it creates a submit event. For suppressing the browser's built-in submit event processing, we invoke the DOM operation preventDefault in a submit event handler like so:

formEl.addEventListener("submit", function (e) {
  e.preventDefault();
  formEl.reset();
});

Finally, still in the module v/createBook.mjs, we set a handler for the event when the browser window (or tab) is closed, taking care to save all data to persistent storage:

window.addEventListener("beforeunload", Book.saveAll);

7.2. Set up the user interface for Update Book

In the UI of the use case Update, which is handled in v/updateBook.mjs, we do not have an input, but rather an output field for the standard identifier attribute isbn, since it is not supposed to be modifiable. Consequently, we don't need to validate any user input for it. However, we need to set up a selection list (in the form of an HTML select element) allowing the user to select a book in the first step, before its data can be modified. This requires to add a change event listener on the select element such that the fields of the HTML form can be filled with the data of the selected object, as taken care of by the following code:

// define variables for accessing UI elements
const formEl = document.forms["Book"],
      saveButton = formEl["commit"],
      selectBookEl = formEl["selectBook"];
// set up the book selection list
fillSelectWithOptions( Book.instances, selectBookEl, "isbn", "title");
// when a book is selected, populate the form with its data
selectBookEl.addEventListener("change", function () {
  const bookKey = selectBookEl.value;
  if (bookKey) {  // set form fields
    const book = Book.instances[bookKey];
    ["isbn","title","year","edition"].forEach( function (p) {
      formEl[p].value = book[p] ? book[p] : "";
      // delete previous custom validation error message
      formEl[p].setCustomValidity("");
    });
  } else {
    formEl.reset();
  }
});

There is no need to set up responsive validation for the standard identifier attribute isbn, but for all other form fields, as shown above for the Create use case.

The logic of v/deleteBook.mjs for the Delete use case is similar. We only need to take care that the object to be deleted can be selected by providing a selection list, like in the Update use case. No validation is needed for the Delete use case.

You can run the validation app from our server or download the code as a ZIP archive file.