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