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