5. Write the User Interface Code

5.1. Show information about associated objects in the List Objects use case

For showing information about the authors of a book in the list books 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.view.books.list = {
  setupUserInterface: function () {
    var tableBodyEl = document.querySelector(
            "section#Book-R>table>tbody");
    var row=null, book=null, listEl=null, 
        keys = Object.keys( Book.instances);
    tableBodyEl.innerHTML = "";  // drop old contents
    for (i=0; i < keys.length; i++) {
      book = Book.instances[keys[i]];
      row = tableBodyEl.insertRow(-1);
      row.insertCell(-1).textContent = book.isbn;
      row.insertCell(-1).textContent = book.title;
      row.insertCell(-1).textContent = book.year;
      // create list of authors
      listEl = util.createListFromMap( 
          book.authors, "name");
      row.insertCell(-1).appendChild( listEl);
      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 (aa, displayProp) {
  var listEl = document.createElement("ul");
  util.fillListFromMap( listEl, aa, displayProp);
  return listEl;
},
fillListFromMap: function (listEl, aa, displayProp) {
  var keys=[], listItemEl=null;
  // drop old contents
  listEl.innerHTML = "";
  // create list items from object property values
  keys = Object.keys( aa);
  for (var j=0; j < keys.length; j++) {
    listItemEl = document.createElement("li");
    listItemEl.textContent = aa[keys[j]][displayProp];
    listEl.appendChild( listItemEl);
  }
},

5.2. Allow 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 book 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="field">
      <label>ISBN: <input type="text" name="isbn" /></label>
    </div>
    <div class="field">
      <label>Title: <input type="text" name="title" /></label>
    </div>
    <div class="field">
      <label>Year: <input type="text" name="year" /></label>
    </div>
    <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>
    <div class="button-group">
      <button type="submit" name="commit">Save</button>
      <button type="button" onclick="pl.view.books.manage.refreshUI()">
       Back to menu</button>
    </div>
  </form>
</section>

The create book 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.view.books.create = {
  setupUserInterface: function () {
    var formEl = document.querySelector("section#Book-C > form"),
        publisherSelectEl = formEl.selectPublisher,
        submitButton = formEl.commit;
    // define event handlers for form field input events
      ...
    // set up the (multiple) authors selection list
    util.fillSelectWithOptions( authorsSelectEl, 
        Author.instances, "authorId", {displayProp:"name"});
    // set up the publisher selection list
    util.fillSelectWithOptions( publisherSelectEl, 
        Publisher.instances, "name");
    ...
  },
  handleSubmitButtonClickEvent: function () {
    ...
  }
};

When the user clicks the submit button, all form control values, including the value of any single-select control, are copied to a corresponding slots record variable, 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 authorsIdRef slot) a list of author ID references from the selected options of the multiple author selection list, as shown in the following program listing:

  handleSubmitButtonClickEvent: function () {
    var i=0, 
        formEl = document.querySelector("section#Book-C > form"),
        selectedAuthorsOptions = formEl.selectAuthors.selectedOptions;
    var slots = {
        isbn: formEl.isbn.value, 
        title: formEl.title.value,
        year: formEl.year.value,
        authorsIdRef: [],
        publisherIdRef: formEl.selectPublisher.value
    };
    // check all input fields 
    ...
    // save the input data only if all of the form fields are valid
    if (formEl.checkValidity()) {
      // construct the list of author ID references
      for (i=0; i < selectedAuthorsOptions.length; i++) {
        slots.authorsIdRef.push( selectedAuthorsOptions[i].value);
      } 
      Book.add( slots);
    }
  }

The update book use case is discussed in the next section.

5.3. Allow selecting associated objects in the update use case

Unfortunatley, the multiple-select control is not really usable for displaying and allowig 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 better use a special multi-select widget that allows to add (and remove) objects to (and from) a list of associated objects, as discussed in 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 book use case.

For allowing to maintain the set of authors associated with the currently edited book in the update book use case, a multi-select 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="field">
      <label>ISBN: <output name="isbn"></output></label>
    </div>
    <div class="field">
      <label>Title: <input type="text" name="title" /></label>
    </div>
    <div class="field">
      <label>Year: <input type="text" name="year" /></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>
    <div class="button-group">
      <button type="submit" name="commit">Save</button>
      <button type="button" 
       onclick="pl.view.books.manage.refreshUI()">Back to menu</button>
    </div>
  </form>
</section>

The update book UI is set up (in the setupUserInterface procedure shown below) by populating

  1. the selection list for selecting the book to be updated with the help of the utility method fillSelectWithOptions, and

  2. the selection list for updating the publisher with the help of the utility method fillSelectWithOptions,

while the multi-select widget for updating the associated authors of the book is only populated (in handleSubmitButtonClickEvent) when a book to be updated has been chosen.

pl.view.books.update = {
  setupUserInterface: function () {
    var formEl = document.querySelector("section#Book-U > form"),
        bookSelectEl = formEl.selectBook,
        publisherSelectEl = formEl.selectPublisher,
        submitButton = formEl.commit;
    // set up the book selection list
    util.fillSelectWithOptions( bookSelectEl, Book.instances, 
        "isbn", {displayProp:"title"});
    bookSelectEl.addEventListener("change", this.handleBookSelectChangeEvent);
    ... // define event handlers for title and year input events
    // set up the associated publisher selection list
    util.fillSelectWithOptions( publisherSelectEl, Publisher.instances, "name");
    // define event handler for submitButton click events    
    submitButton.addEventListener("click", this.handleSubmitButtonClickEvent);
    // define event handler for neutralizing the submit event and reseting 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 () {
  var formEl = document.querySelector("section#Book-U > form"),
      authorsSelWidget = formEl.querySelector(
          ".MultiSelectionWidget"),
      key = formEl.selectBook.value,
      book=null;
  if (key !== "") {
    book = Book.instances[key];
    formEl.isbn.value = book.isbn;
    formEl.title.value = book.title;
    formEl.year.value = book.year;
    // set up a multi-select widget for associated authors
    util.createMultiSelectionWidget( authorsSelWidget, 
        book.authors, Author.instances, "authorId", "name");
    // assign associated publisher to index of select element  
    formEl.selectPublisher.selectedIndex = 
        (book.publisher) ? book.publisher.index : 0;
  } else {
    formEl.reset();
    formEl.selectPublisher.selectedIndex = 0;
  }
},

When the user, after updating some values, finally clicks the submit 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 authorsIdRefToAdd and authorsIdRefToRemove 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:

handleSubmitButtonClickEvent: function () {
  var i=0, assocAuthorListItemEl=null, 
      authorsIdRefToAdd=[], authorsIdRefToRemove=[],
      formEl = document.querySelector("section#Book-U > form"),
      authorsSelWidget = 
          formEl.querySelector(".MultiSelectionWidget"),
      authorsAssocListEl = authorsSelWidget.firstElementChild;
  var slots = { isbn: formEl.isbn.value, 
        title: formEl.title.value,
        year: formEl.year.value,
        publisherIdRef: formEl.selectPublisher.value
      };
  // commit the update only if all of the form fields values are valid
  if (formEl.checkValidity()) {
    // construct authorsIdRefToAdd and authorsIdRefToRemove
    for (i=0; i < authorsAssocListEl.children.length; i++) {
      assocAuthorListItemEl = authorsAssocListEl.children[i]; 
      if (assocAuthorListItemEl.classList.contains("removed")) {
        authorsIdRefToRemove.push( 
            assocAuthorListItemEl.getAttribute("data-value"));          
      }
      if (assocAuthorListItemEl.classList.contains("added")) {
        authorsIdRefToAdd.push( 
            assocAuthorListItemEl.getAttribute("data-value"));          
      }
    } 
    // if the add/remove list is non-empty create a corresponding slot
    if (authorsIdRefToRemove.length > 0) {
      slots.authorsIdRefToRemove = authorsIdRefToRemove;
    }
    if (authorsIdRefToAdd.length > 0) {
      slots.authorsIdRefToAdd = authorsIdRefToAdd;
    }
    Book.update( slots);
  }
}