The user interface (UI) consists of a start page
index.html
that allows the user choosing one of the
data management operations by navigating to the corresponding UI page such
as retrieveAndListAllBooks.html
or
createBook.html
.
The start page index.html
has been discussed
above in Section 5.2. It sets up two buttons for clearing the app's
database by invoking the procedure Book.clearData()
and for
creating sample data by invoking the procedure
Book.createTestData()
from the buttons' click
event listeners.
Each data management UI page useCase.html
loads the same
basic CSS and JavaScript files like the start page
index.html
discussed above. In addition, it loads a
use-case-specific view module file src/v/
useCase.mjs
. The CSS file
main.css
now contains the following rule for marking
invalid form fields by drawing a red outline:
form:invalid {
outline: dotted red;
}
For setting up the user interfaces of the data management use cases, we have to distinguish the case of "Retrieve/List All" from the other ones (Create, Update, Delete). While the latter ones require using an HTML form and attaching event handlers to form controls, in the case of "Retrieve/List All" we only have to render a table displaying all books, as in the case of the Minimal App discussed in Chapter 3.
For all four CRUD user interfaces, in the use-case-specific view
module file src/v/
useCase.mjs
, we need three
code blocks for
importing the Book
class and possibly other
items,
loading data (in particular, all book records), and
defining variables for accessing various UI elements.
These three steps may look like so:
// import classes and other items import Book from "../m/Book.mjs"; // load data Book.retrieveAll(); // define variables for accessing UI elements const formEl = document.forms["Book"], saveButton = formEl["commit"];
In addition, in the three cases of Create, Update and Delete, we have to add several code blocks for defining event listeners for:
responsive validation on form field input
events,
handling the event when the user pushes the save (or delete) button,
making sure the main memory data is saved when a
beforeunload
event occurs, that is, when the browser
window/tab is closed.
For the use case Create, we
obtain the following code (in v/createBook.mjs
) for
adding event listeners for responsive
validation:
formEl.isbn.addEventListener("input", function () {formEl.isbn.setCustomValidity( Book.checkIsbnAsId( formEl.isbn.value).message); }); formEl.title.addEventListener("input", function () {formEl.title.setCustomValidity( Book.checkTitle( formEl.title.value).message); }); formEl.year.addEventListener("input", function () {formEl.year.setCustomValidity( Book.checkYear( formEl.year.value).message); }); formEl.edition.addEventListener("input", function () {formEl.edition.setCustomValidity( Book.checkEdition( formEl.edition.value).message); });
Notice that for each input field we add a listener for
input
events, such that on any user input a validation
check is performed because input
events are created by user
input actions such as typing. We use the predefined function
setCustomValidity
from the HTML5 form validation
API for having our property check functions invoked on the
current value of the form field and returning an error message in the
case of a constraint violation. So, whenever the string represented by
the expression Book.checkIsbn( formEl.isbn.value).message
is empty, everything is fine. Otherwise, if it represents an error
message, the browser indicates the constraint violation to the user by
rendering a red outline for the form field concerned (due to our CSS
rule for the :invalid
pseudo class).
In addition to the event handlers for responsive constraint validation, we need two more event handlers: one for validation on form data submission and one for the event when the browser window (or tab) is closed.
While the validation on user input enhances the usability of the
UI by providing immediate feedback to the user, validation on form data
submission is even more important for catching invalid data. In the form
data submission event handler, the property checks are performed again
(with the help of setCustomValidity
), as shown in the
following program listing:
saveButton.addEventListener("click", function () { const slots = { isbn: formEl.isbn.value, title: formEl.title.value, year: formEl.year.value }; // set error messages in case of constraint violations formEl.isbn.setCustomValidity( Book.checkIsbnAsId( slots.isbn).message); formEl.title.setCustomValidity( Book.checkTitle( slots.title).message); formEl.year.setCustomValidity( Book.checkYear( slots.year).message); if (formEl.edition.value) { slots.edition = formEl.edition.value; formEl.edition.setCustomValidity( Book.checkEdition( slots.edition).message); } // save the input data only if all of the form fields are valid if (formEl.checkValidity()) Book.add( slots); });
By invoking checkValidity()
on the form element, we
make sure that the form data is only saved (by Book.add
),
if there is no constraint violation. After this event handler has been
executed on an invalid form, the browser takes control and tests if the
predefined property validity
has an error flag for any form
field. In our approach, since we use setCustomValidity
, the
validity.customError
would be true. If this is the case,
the custom constraint violation message will be displayed (in a
bubble).
Since the Save button has the type "submit",
clicking it creates a submit event. For suppressing
the browser's built-in submit event processing, we
invoke the DOM operation preventDefault
in a
submit event handler like so:
formEl.addEventListener("submit", function (e) { e.preventDefault(); formEl.reset(); });
Finally, still in the module
v/createBook.mjs
, we set a handler for the event
when the browser window (or tab) is closed, taking care to save all data
to persistent storage:
window.addEventListener("beforeunload", Book.saveAll);
In the UI of the use case Update, which is handled in
v/updateBook.mjs
, we do not have an
input
, but rather an output
field for the
standard identifier attribute isbn
, since it is not
supposed to be modifiable. Consequently, we don't need to validate any
user input for it. However, we need to set up a selection list (in the
form of an HTML select
element) allowing the user to select
a book in the first step, before its data can be modified. This requires
to add a change
event listener on the select
element such that the fields of the HTML form can be filled with the
data of the selected object, as taken care of by the following
code:
// define variables for accessing UI elements const formEl = document.forms["Book"], saveButton = formEl["commit"], selectBookEl = formEl["selectBook"]; // set up the book selection list fillSelectWithOptions( Book.instances, selectBookEl, "isbn", "title"); // when a book is selected, populate the form with its data selectBookEl.addEventListener("change", function () { const bookKey = selectBookEl.value; if (bookKey) { // set form fields const book = Book.instances[bookKey]; ["isbn","title","year","edition"].forEach( function (p) { formEl[p].value = book[p] ? book[p] : ""; // delete previous custom validation error message formEl[p].setCustomValidity(""); }); } else { formEl.reset(); } });
There is no need to set up responsive validation for the standard
identifier attribute isbn
, but for all other form fields,
as shown above for the Create use
case.
The logic of v/deleteBook.mjs
for the
Delete use case is similar. We only
need to take care that the object to be deleted can be selected by
providing a selection list, like in the Update use case. No validation is needed for
the Delete use case.
You can run the validation app from our server or download the code as a ZIP archive file.