Exercise: Reading List
Make a reading-list page work, reading its add-book form, refusing a book with no title or 0 pages with a message next to the field, and saving the list in localStorage so it's still there tomorrow, with tests for the rules and a checklist for the page.
Journeyman · JavaScript · HTML · Web Development · Updated
My Reading List is a page for keeping track of books: the ones you want to read, the one you're reading now, and the ones you've finished. The page is built and styled, and the form is there, but pressing Add book does nothing useful yet: it reloads the page and forgets everything. You'll write the JavaScript that reads the form, refuses a book that isn't filled in right (with a clear message next to the field), adds it to the list, and remembers the list between visits. Tests check the rules, and a checklist checks the page.
Get the exercise code
In the Exercise box beside the lesson, choose Get the starter on GitHub to open its GitHub repository. Click Code, then Download ZIP, and unzip it where you keep your practice folders. Or clone it with Git.
The folder holds:
index.html,styles.css: the page and its look. Ready to use. The form already hasrequired,min="1",novalidate, and a message paragraph under the Title and Pages boxes, tied to each box witharia-describedby.books.js: three functions that check a book and turn the list into text and back, with no page involved. For you to finish.app.js: connects the page to those functions. It already draws the list, withrenderBooks. Its four TODO comments are for you to finish.books.test.js: the tests forbooks.js. You don't need to change it.package.json: tells Node.js that these files useimportandexport. Ready to use.
Open the folder in VS Code, and open a terminal there with Terminal > New Terminal.
What to do
1. The rules, in books.js. Each function is declared and gives back a placeholder for now. The comment above it says what it should give back, with examples. Here's the first:
// 1. validateBook(book)
// Return an object with a message for each field that's wrong, or an empty object, {}, when the book is fine.
// - The title is wrong when it's empty or only spaces. Its message: 'Every book needs a title.'
// - The pages are wrong unless they're a whole number, 1 or more (0, -5, 2.5 and NaN are all wrong).
// Their message: 'Pages must be a whole number, 1 or more.'
// - The author may be empty: some books don't have one you know.
export function validateBook(book) {
return null;
}
validateBook(book)checks one book and gives back its error messages, keyed by field name.toStorage(books)gives back the text to save in localStorage for the whole list.fromStorage(saved)turns that text back into the list. It gives back an empty list,[], when nothing is saved yet (null), when the text is broken, and when the text holds something that isn't a list.
2. The page, in app.js. The four TODOs, in order:
- Load the books saved last time, instead of starting with an empty list.
- Show a field's message in the paragraph under it, and mark the field with
aria-invalid="true"; an empty message clears both. - When the form is sent, stop it from reloading the page, and build a book object from its fields.
- Check the book. If anything is wrong, show the messages, put the cursor in the first wrong field and stop. Otherwise add the book, save the list, draw it again, empty the form and put the cursor back in the Title box.
Check your work
Run the tests from the terminal:
node --test
At first all 9 tests fail. That's expected: each function you finish turns its tests green. When a test fails, its message says what it expected, for example On a first visit getItem gives null, and the reading list starts empty.
Then open index.html with Live Preview in your browser, and work through the checklist in the folder's README.md. It walks through adding a book, each wrong field, a reload, and a first visit with nothing saved. Press F12 and look at the Console whenever the page doesn't do what you expect: an error there names the file and line.
Hints
- Keep the thinking out of the page.
books.jsnever touches the page, so Node can test it.app.jsreads the form, calls those functions, and writes the answers back to the page. The more of the work sits inbooks.js, the more of it the tests prove. - Text in, number out. The Pages box gives text, even though it's a number box. Reading What People Type shows how to turn it into a number, and what an empty box turns into.
- Spaces aren't a title. Trim the title before you check it (String Methods), and look up
Number.isIntegerfor "a whole number". - null, broken, or not a list? Remembering Data with localStorage shows the first two cases.
Array.isArray(value)answers the third. - Listen for submit on the form, not for clicks on the button, so pressing Enter works too, and cancel the reload with
event.preventDefault()(Canceling Events). - One small function for messages. Checking a Form with JavaScript shows how to put a message under a field and mark it with
aria-invalid. Writing that once, as a function that takes the field's name and the message, saves writing it four times.
When you're done
All 9 tests are green, and the test run's summary says:
ℹ tests 9
ℹ suites 0
ℹ pass 9
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
and every box on the checklist is ticked: books are added without a reload, wrong fields get their message right under them, and the list is still there after you close the browser and come back.
Now look back at app.js. Is there any rule in it, such as "a title can't be only spaces", that the tests can't see? If there is, could it move into books.js?
So what
Read a form, check it kindly, keep the data, draw it again: that's the loop behind a to-do app, a shopping list, a workout log and a thousand other small apps people use every day. You've built the whole loop yourself, with no framework and no server.