Exercise: Recipe Filters

Bring the Weeknight Recipes page to life, drawing its cards from data, filtering them with buttons, opening each card's steps with one delegated listener and closing them with Escape, and let the tests and a checklist confirm it works.

Journeyman · JavaScript · Web Development · Updated

Weeknight Recipes is a small recipe site for busy evenings, and it's almost ready: the page is designed, the styles are done, and six recipes are waiting in a data file. What's missing is the JavaScript. Right now the page shows two cards that were typed into the HTML by hand, and none of its buttons does anything. You'll write the code that draws every card from the data, filters them by meal, saves favorites with a heart, opens and closes each recipe's steps, and works from the keyboard as well as the mouse. Tests check the two functions that do the thinking, and a checklist walks you through 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 into the folder where you keep this course's work. Or clone it with Git.

File What's in it
index.html, styles.css The page, finished. Its two cards show what a card looks like; your code replaces them.
recipe-data.js The six recipes, as an array of objects
cards.js makeCard(recipe, isOpen), which builds one card. Ready to use; read it once.
recipes.js filterRecipes and toggleFavorite, two functions with no page in them. For you to finish.
app.js The page code: drawing the cards and answering clicks and keys. For you to finish.
recipes.test.js The tests for recipes.js. You don't need to change it.

Open the folder in VS Code, and open a terminal there with Terminal > New Terminal.

What to do

Every function you finish is already declared, and the comment above it says what it should do, with examples. This is how recipes.js starts:

/*
 * filterRecipes(recipes, 'dinner')    -> [Sheet-Pan Fajitas, Veggie Fried Rice]
 * filterRecipes(recipes, 'favorites') -> [Sheet-Pan Fajitas]
 */
export function filterRecipes(recipes, meal) {
  return [];
}
  1. Finish filterRecipes. It gives back the recipes a filter button shows: every recipe for 'all', one meal's recipes for 'breakfast', 'lunch' or 'dinner', and the hearted ones for 'favorites'.
  2. Finish toggleFavorite. It gives back a new array in which one recipe's heart is flipped, and it leaves the array it was given exactly as it was.
  3. Finish app.js, in the order of its numbered comments:
    • render() draws the page from the data: the cards, the count, and which filter button is pressed.
    • One listener on the filter bar chooses the meal.
    • One listener on the recipe list handles every card's Show steps and heart.
    • One keydown listener on the document closes the open steps with Escape.

The rule that holds it together: a listener changes the data (recipes, meal or openId), then calls render(). No listener changes a card by hand.

Check your work

Run the tests from the exercise folder:

node --test

At first every test fails. That's expected: each function you finish turns its tests green. When a test fails, its message says what it expected, for example filterRecipes(recipes, 'dinner') should give back the two dinner recipes, in order.

Then open index.html with Live Preview and work through the checklist in the exercise's README.md. It covers what the tests can't: the filters, the hearts, the steps, and the whole page with the keyboard only. Press F12 to see the Console when something on the page doesn't work.

Hints

  • Do the thinking first. recipes.js has no page in it, so the tests can check it on its own. Get them green, and app.js only has to read the page, call your functions and draw.
  • New arrays, not changed ones. filter and map both give back a new array. Spread, { ...recipe }, from Spread and Rest, copies one recipe so you can change the copy.
  • One listener, many buttons. Both buttons on every card are inside the list, so their clicks bubble up to it. closest, from Bubbling and Event Delegation, finds the button even when the click lands on the heart's icon, and then the card.
  • Which button was it? classList.contains('favorite') tells the heart apart from Show steps, and the card's dataset.id says which recipe it is.
  • Open, or closed? Only one card is open at a time, and openId remembers which. Clicking Show steps on the open card closes it again.
  • Focus after render. render makes new buttons, so the one that was pressed is gone. focusOn(id, 'show-steps'), ready in app.js, puts the focus ring on the new one, as Drawing the Page from Your Data explains.

When you're done

Every test is green:

✔ filterRecipes: all gives every recipe, in order (0.961ms)
✔ filterRecipes: a meal gives only that meal, and a meal with no recipes gives an empty array (0.6831ms)
✔ filterRecipes: favorites gives the hearted recipes, and the list it was given is unchanged (0.2115ms)
✔ toggleFavorite: turns a heart on, and the original recipe keeps its old value (0.1323ms)
✔ toggleFavorite: turns a heart off (0.0736ms)
✔ toggleFavorite: changes only the recipe with that id (0.0907ms)
✔ toggleFavorite: always gives back a new array, even for an id that is not there (0.1102ms)
ℹ tests 7
ℹ suites 0
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 66.4784

Every line of the checklist works, with the mouse and with the keyboard. Now add a seventh recipe to recipe-data.js, a snack, and a Snacks button to the filter bar in index.html with data-meal="snack". How many lines of app.js did you have to change? If the answer is none, your page really is drawn from its data.

So what

Data in an array, one render function, a listener per group of controls, and keyboard support from the start: that's the shape of every interactive list on the web, and the photo gallery at the end of this module uses the same shape.

Key terms

Related