5. Code the View

5.1. Setting up the Retrieve/List All user interface

For showing information about the authors of a book in the view table of the Retrieve/List All user interface, the corresponding cell in the HTML table is filled (in v/books.mjs) with a list of the names of all authors with the help of the utility function createListFromMap:

const tableBodyEl = document.querySelector("section#Book-R>table>tbody");
tableBodyEl.innerHTML = "";  // drop old content
for (const key of Object.keys( Book.instances)) {
  const book = Book.instances[key];
  // create list of authors for this book
  const authListEl = createListFromMap( book.authors, "name");
  const row = tableBodyEl.insertRow();
  row.insertCell().textContent = book.isbn;
  row.insertCell().textContent = book.title;
  row.insertCell().textContent = book.year;
  row.insertCell().appendChild( authListEl);
  // if the book has a publisher, show its name
  row.insertCell().textContent =
      book.publisher ? book.publisher.name : "";
}

The utility function createListFromMap (in lib/util.mjs) has the following code:

function createListFromMap( entityTbl, displayProp) {
  const listEl = document.createElement("ul");
  // delete old contents
  listEl.innerHTML = "";
  // create list items from object property values
  for (const key of Object.keys( entityTbl)) {
    const listItemEl = document.createElement("li");
    listItemEl.textContent = entityTbl[key][displayProp];
    listEl.appendChild( listItemEl);
  }
  return listEl;
}

5.2. Selecting associated objects in the Create user interface

For allowing to select multiple authors to be associated with the currently edited book in the Create user interface, a multiple selection list (a select element with the multiple attribute set to "multiple"), as shown in the HTML code below (from books.html), is populated with the instances of the associated object type.

<section id="Book-C" class="UI-Page">
 <h1>Public Library: Create a new book record</h1>
 <form>
  ... 
  <div class="select-one">
   <label>Publisher: <select name="selectPublisher"></select></label>
  </div>
  <div class="select-many">
   <label>Authors: 
    <select name="selectAuthors" multiple="multiple"></select>
   </label>
  </div>
  ...
 </form>
</section>

The Create UI is set up by populating selection lists for selecting the authors and the publisher with the help of a utility method fillSelectWithOptions as shown in the following program listing (from v/books.mjs):

const createFormEl = document.querySelector("section#Book-C > form"),
      selectAuthorsEl = createFormEl["selectAuthors"],
      selectPublisherEl = createFormEl["selectPublisher"];
document.getElementById("create").addEventListener("click", function () {
  // set up a single selection list for selecting a publisher
  fillSelectWithOptions( selectPublisherEl, Publisher.instances, "name");
  // set up a multiple selection list for selecting authors
  fillSelectWithOptions( selectAuthorsEl, Author.instances,
      "authorId", {displayProp: "name"});
  document.getElementById("Book-M").style.display = "none";
  document.getElementById("Book-C").style.display = "block";
  createFormEl.reset();
});
// set up event handlers for responsive constraint validation
...
// handle Save button click events
createFormEl["commit"].addEventListener("click", function () {
  ...
});

When the user clicks the Save button, all form control values, including the value of any single-select control, are copied to a corresponding field of the slots record, which is used as the argument for invoking the add method after all form fields have been checked for validity. Before invoking add, we first have to create (in the authorIdRefs slot) a list of author ID references from the selected options of the multiple authors selection list, as shown in the following program listing:

// handle Save button click events
createFormEl["commit"].addEventListener("click", function () {
  const slots = {
    isbn: createFormEl["isbn"].value,
    title: createFormEl["title"].value,
    year: createFormEl["year"].value,
    authorIdRefs: [],
    publisher_id: createFormEl["selectPublisher"].value
  };
  // check all input fields and show validation error messages
  ...
  // get the list of selected authors
  const selAuthOptions = createFormEl["selectAuthors"].selectedOptions;
  // check the mandatory value constraint for authors
  createFormEl["selectAuthors"].setCustomValidity(
      selAuthOptions.length > 0 ? "" : "No author selected!");
  // save the input data only if all form fields are valid
  if (createFormEl.checkValidity()) {
    // construct a list of author ID references
    for (const opt of selAuthOptions) {
      slots.authorIdRefs.push( opt.value);
    }
    Book.add( slots);
  }
});

The Update use case is discussed in the next section.

5.3. Selecting associated objects in the Update user interface

Unfortunately, HTML's multiple-select control is not really usable for displaying and allowing to maintain the set of associated authors in realistic use cases where we have several hundreds or thousands of authors, because the way it renders the choice in a large list to be scrolled is visually too scattered, violating general usability requirements. So we have to use a special multi-selection widget that allows to add (and remove) objects to (and from) a list of associated objects, as discussed in Section 8. In order to show how this widget can replace the multiple-selection list discussed in the previous section, we use it now in the Update use case.

For allowing to maintain the set of authors associated with the currently edited book in the Update use case, a multi-selection widget as shown in the HTML code below, is populated with the instances of the Author class.

<section id="Book-U" class="UI-Page">
  <h1>Public Library: Update a book record</h1>
  <form>
    <div class="select-one">
      <label>Select book: <select name="selectBook"></select></label>
    </div>
    ...
    <div class="select-one">
      <label>Publisher: <select name="selectPublisher"></select></label>
    </div>
    <div class="widget">
      <label for="updBookSelectAuthors">Authors: </label>
      <div class="MultiSelectionWidget" id="updBookSelectAuthors"></div>
    </div>
    ...
  </form>
</section>

The Update user interface is set up (in a section of v/books.mjs) by populating the selection list for selecting the book to be updated with the help of the utility method fillSelectWithOptions.

const updateFormEl = document.querySelector("section#Book-U > form"),
      updSelBookEl = updateFormEl["selectBook"];
document.getElementById("update").addEventListener("click", function () {
  document.getElementById("Book-M").style.display = "none";
  document.getElementById("Book-U").style.display = "block";
  // set up the book selection list
  fillSelectWithOptions( updSelBookEl, Book.instances,
      "isbn", {displayProp: "title"});
  updateFormEl.reset();
});

The selection list for assigning a publisher and the multi-selection widget for assigning the authors of a book are only populated after a book to be updated has been chosen in the books selection list. The following event handler that listens to change events on the select element with name "selectBook" takes care of this:

updSelBookEl.addEventListener("change", function () {
  const saveButton = updateFormEl["commit"],
    selectAuthorsWidget = updateFormEl.querySelector(".MultiSelectionWidget"),
    selectPublisherEl = updateFormEl["selectPublisher"],
    isbn = updateFormEl["selectBook"].value;
  if (isbn) {
    const book = Book.instances[isbn];
    updateFormEl["isbn"].value = book.isbn;
    updateFormEl["title"].value = book.title;
    updateFormEl["year"].value = book.year;
    // set up the associated publisher selection list
    fillSelectWithOptions( selectPublisherEl, Publisher.instances, "name");
    // set up the associated authors selection widget
    createMultiSelectionWidget( selectAuthorsWidget, book.authors,
        Author.instances, "authorId", "name", 1);  // minCard=1
    // assign associated publisher as the selected option to select element
    if (book.publisher) {
      updateFormEl["selectPublisher"].value = book.publisher.name;
    }
    saveButton.disabled = false;
  } else {
    updateFormEl.reset();
    updateFormEl["selectPublisher"].selectedIndex = 0;
    selectAuthorsWidget.innerHTML = "";
    saveButton.disabled = true;
  }
});

When a book to be updated has been chosen, the output field isbn and the input fields title and year, as well as the selection field for updating the publisher, are assigned corresponding values from the chosen book, and the associated authors selection widget is set up with the help of the utility procedure createMultiSelectionWidget.

When the user, after updating some values, finally clicks the Save button, all form control values, including the value of the single-select control for assigning a publisher, are copied to corresponding slots in a slots record variable, which is used as the argument for invoking the Book.update method after all values have been checked for validity. Before invoking update, a list of ID references to authors to be added, and another list of ID references to authors to be removed, is created (in the authorIdRefsToAdd and authorIdRefsToRemove slots) from the updates that have been recorded in the associated authors selection widget with "added" and "removed" as values of the corresponding list item's class attribute, as shown in the following program listing:

updateFormEl["commit"].addEventListener("click", function () {
  const bookIdRef = updSelBookEl.value,
    selectAuthorsWidget = updateFormEl.querySelector(".MultiSelectionWidget"),
    selectedAuthorsListEl = selectAuthorsWidget.firstElementChild;
  if (!bookIdRef) return;
  const slots = {
    isbn: updateFormEl["isbn"].value,
    title: updateFormEl["title"].value,
    year: updateFormEl["year"].value,
    publisher_id: updateFormEl["selectPublisher"].value
  };
  // add event listeners for responsive validation
  ...
  // commit the update only if all form field values are valid
  if (updateFormEl.checkValidity()) {
    // construct authorIdRefs-ToAdd/ToRemove lists
    const authorIdRefsToAdd=[], authorIdRefsToRemove=[];
    for (const authorItemEl of selectedAuthorsListEl.children) {
      if (authorItemEl.classList.contains("removed")) {
        authorIdRefsToRemove.push( authorItemEl.getAttribute("data-value"));
      }
      if (authorItemEl.classList.contains("added")) {
        authorIdRefsToAdd.push( authorItemEl.getAttribute("data-value"));
      }
    }
    // if the add/remove list is non-empty, create a corresponding slot
    if (authorIdRefsToRemove.length > 0) {
      slots.authorIdRefsToRemove = authorIdRefsToRemove;
    }
    if (authorIdRefsToAdd.length > 0) {
      slots.authorIdRefsToAdd = authorIdRefsToAdd;
    }
    Book.update( slots);
    // update the book selection list's option element
    updSelBookEl.options[updSelBookEl.selectedIndex].text = slots.title;
    // drop widget content
    selectAuthorsWidget.innerHTML = "";
  }
});

You can run the example app from our server and download it as a ZIP archive file.