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.