5. Code the View

5.1. Showing information about associated objects in Retrieve/List All

For showing information about the authors of a book in the Retrieve/List All use case, the corresponding cell in the HTML table is filled with a list of the names of all authors with the help of the utility function util.createListFromMap:

pl.v.books.retrieveAndListAll = {
  setupUserInterface: function () {
    const tableBodyEl = document.querySelector(
	                  "section#Book-R>table>tbody");
    tableBodyEl.innerHTML = "";  // drop old contents
    for (let key of Object.keys( Book.instances)) {
      const book = Book.instances[key];
      // create list of authors for this book
      const listEl = util.createListFromMap( book.authors, "name");
      const row = tableBodyEl.insertRow(-1);
      row.insertCell(-1).textContent = book.isbn;
      row.insertCell(-1).textContent = book.title;
      row.insertCell(-1).textContent = book.year;
      row.insertCell(-1).appendChild( listEl);
      // if the book has a publisher, show its name
      row.insertCell(-1).textContent = 
	      book.publisher ? book.publisher.name : "";
    }
    document.getElementById("Book-M").style.display = "none";
    document.getElementById("Book-R").style.display = "block";
  }
};

The utility function util.createListFromMap has the following code:

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

5.2. Selecting associated objects in the Create use case

For allowing to select multiple authors to be associated with the currently edited book in the Create use case, a multiple selection list (a select element with multiple="multiple"), as shown in the HTML code below, 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:

pl.v.books.create = {
  setupUserInterface: function () {
    const formEl = document.querySelector("section#Book-C > form"),
        selectAuthorsEl = formEl.selectAuthors,
        selectPublisherEl = formEl.selectPublisher,
        saveButton = formEl.commit;
    // add event listeners for responsive validation 
    ...
    // set up a multiple selection list for selecting authors
    util.fillSelectWithOptions( selectAuthorsEl, Author.instances,
	      "authorId", {displayProp:"name"});
    // set up a single selection list for selecting a publisher
    util.fillSelectWithOptions( selectPublisherEl,
	      Publisher.instances, "name");
    ...
  },
  handleSaveButtonClickEvent: 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:

handleSaveButtonClickEvent: function () {
  const formEl = document.querySelector("section#Book-C > form"),
      selAuthOptions = formEl.selectAuthors.selectedOptions;
  const slots = {
      isbn: formEl.isbn.value, 
      title: formEl.title.value,
      year: formEl.year.value,
      authorIdRefs: [],
      publisher_id: formEl.selectPublisher.value
  };
  // validate all form controls and show error messages 
  ...
  // check the mandatory value constraint for authors
  formEl.selectAuthors.setCustomValidity(
    (selAuthOptions.length > 0) ? "" : "No author selected!"
  );
  // save the input data only if all form fields are valid
  if (formEl.checkValidity()) {
    // construct a list of author ID references
    for (let 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 use case

Unfortunately, the 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 is visually too scattered. So we have to use a special multiple-choice 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 multiple-choice 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>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 the setupUserInterface procedure shown below) by populating the selection list for selecting the book to be updated with the help of the utility method fillSelectWithOptions. The selection list for assigning a publisher and the multiple-choice widget for assigning the authors of a book are only populated (in handleSubmitButtonClickEvent) when a book to be updated has been chosen.

pl.v.books.update = {
  setupUserInterface: function () {
    const formEl = document.querySelector("section#Book-U > form"),
        selectBookEl = formEl.selectBook,
        saveButton = formEl.commit;
    // set up the book selection list
    util.fillSelectWithOptions( selectBookEl, Book.instances,
        "isbn", {displayProp:"title"});
    selectBookEl.addEventListener("change", this.handleBookSelectChangeEvent);
    // add event listeners for responsive validation
    ...
    // define event handler for submit button click events    
    saveButton.addEventListener("click", this.handleSaveButtonClickEvent);
    // handle neutralizing the submit event and resetting the form
    formEl.addEventListener( 'submit', function (e) {
      var authorsSelWidget = document.querySelector(
              "section#Book-U > form .MultiSelectionWidget");
      e.preventDefault();
      authorsSelWidget.innerHTML = "";
      formEl.reset();
    });
    document.getElementById("Book-M").style.display = "none";
    document.getElementById("Book-U").style.display = "block";
    formEl.reset();
  },

When a book to be updated has been chosen, the form input fields isbn, title and year, and the select control for updating the publisher, are assigned corresponding values from the chosen book, and the associated authors selection widget is set up:

handleBookSelectChangeEvent: function () {
  const formEl = document.querySelector("section#Book-U > form"),
      saveButton = formEl.commit,
      selectAuthorsWidget = formEl.querySelector(".MultiChoiceWidget"),
      selectPublisherEl = formEl.selectPublisher,
      isbn = formEl.selectBook.value;
  if (isbn !== "") {
    let book = Book.instances[isbn];
    formEl.isbn.value = book.isbn;
    formEl.title.value = book.title;
    formEl.year.value = book.year;
    // set up the associated authors selection widget
    util.createMultipleChoiceWidget( selectAuthorsWidget, book.authors,
        Author.instances, "authorId", "name", 1);  // minCard=1
    // set up the associated publisher selection list
    util.fillSelectWithOptions( selectPublisherEl, Publisher.instances, "name");
    // assign associated publisher as the selected option to select element
    if (book.publisher) formEl.selectPublisher.value = book.publisher.name;
    saveButton.disabled = false;
  } else {
    formEl.reset();
    formEl.selectPublisher.selectedIndex = 0;
    selectAuthorsWidget.innerHTML = "";
    saveButton.disabled = true;
  }
},

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 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 the help of classList values, as shown in the following program listing:

handleSaveButtonClickEvent: function () {
  const formEl = document.querySelector("section#Book-U > form"),
      selectBookEl = formEl.selectBook,
      selectAuthorsWidget = formEl.querySelector(".MultiChoiceWidget"),
      multiChoiceListEl = selectAuthorsWidget.firstElementChild;
  const slots = { isbn: formEl.isbn.value, 
        title: formEl.title.value,
        year: formEl.year.value,
        publisher_id: formEl.selectPublisher.value
      };
  // add event listeners for responsive validation
  ...
  // commit the update only if all form field values are valid
  if (formEl.checkValidity()) {
    // construct authorIdRefs-ToAdd/ToRemove lists from the association list
    let authorIdRefsToAdd=[], authorIdRefsToRemove=[];
    for (let mcListItemEl of multiChoiceListEl.children) {
      if (mcListItemEl.classList.contains("removed")) {
        authorIdRefsToRemove.push( mcListItemEl.getAttribute("data-value"));
      }
      if (mcListItemEl.classList.contains("added")) {
        authorIdRefsToAdd.push( mcListItemEl.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
    selectBookEl.options[selectBookEl.selectedIndex].text = slots.title;
  }
}