Skip to main content
← JavaScript

Lesson 5 of 11

Creature Finder

A list you can search and filter. Three array methods do all of the work.

What you will learn

  • .filter()
  • .map()
  • .includes()
  • .toLowerCase()
  • Redrawing from the data
  • data- attributes
  • The empty state

Before you start

Quote Typer. There you handled typing and read one item out of an array of objects. Here you work with the whole array at once.

What this page does

  1. Type in the box. The list shrinks as you type.
  2. Click Sea. It shrinks again, and the search still applies.
  3. Type something no creature matches. A message appears instead of a blank page.
  4. Clear the box and click All. Everything comes back.

Three methods, three jobs

Each one takes a function and runs it once for every item in the array. None of them changes the original array.

MethodIts one jobWhat you get back
.filter()Keep some items, drop the restA new, shorter array
.map()Turn every item into something elseA new array, the same length
.forEach()Do a job with every itemNothing

The whole of the drawing is those three, one after another. Read it as one sentence: keep the ones that match, turn each of those into a card, then put every card on the page.

finder.js, inside draw()
const found = creatures.filter(matches);
const cards = found.map(makeCard);
cards.forEach(function (card) {
    list.appendChild(card);
});

The test that filter runs

filter calls matches once for each creature. That function gets one creature and answers true or false. True keeps it.

finder.js
function matches(creature) {
    const rightHabitat = habitat === "all" || creature.habitat === habitat;
    const rightName = creature.name.toLowerCase().includes(searchText);
    return rightHabitat && rightName;
}
  • .includes() asks whether one piece of text is inside another. "Sea Otter".includes("ott") is false, because of the capital letters.
  • .toLowerCase() on both sides is what fixes that. The name is lowered here, and the search text was lowered when it was typed.
  • && means both halves must be true. That is what lets the search box and the buttons work together with no extra code.
  • habitat === "all" || is the escape hatch. When All is chosen, the first half is true and the habitat stops mattering.

Where the buttons keep their answer

index.html
<button class="chip" data-habitat="sea">Sea</button>

Any attribute starting with data- is one you invented. The browser ignores it and your code can read it. data-habitat becomes button.dataset.habitat in JavaScript.

This is why one listener handles all four buttons. The code never asks which button was pressed. It asks the button that was pressed what it stands for.

chips.forEach(function (chip) {
    chip.addEventListener("click", function () {
        habitat = chip.dataset.habitat;
        draw();
    });
});

A blank page looks broken

When nothing matches, an empty list is indistinguishable from a page that has crashed. So the code says why.

if (found.length === 0) {
    const nothing = document.createElement("p");
    nothing.className = "empty";
    nothing.textContent = "No creatures match. Try a shorter search.";
    list.appendChild(nothing);
}

Build it step by step

Get all twelve on the page before you filter any of them. A filter you cannot see working is hard to debug.

  1. Make the page

    Make index.html with a search box, four buttons and one empty <div id="list">. Put the <script> tag at the end of <body>. Nothing shows yet.

  2. Write the data

    In finder.js, make the creatures array. Each item is an object with name, habitat, art and fact. Four or five is enough to start.

  3. Draw all of them

    Write makeCard, then a draw function that loops the whole array and appends every card. No filtering yet. Reload. Every creature appears.

  4. Filter by name

    Add the search box listener. Store what was typed in searchText, then use creatures.filter(...) in draw. Test it by typing part of one name.

    search.addEventListener("input", function () {
        searchText = search.value.toLowerCase().trim();
        draw();
    });
  5. Add the habitat buttons

    Add the data-habitat attributes and one listener for all four. Store the answer in habitat and add that half to matches. Now both work at once, and you wrote no code to combine them.

  6. Say when nothing matched

    Add the empty state. Test it by typing letters no creature has.

  7. Show the count

    Write found.length and creatures.length into the count line. This is the fastest way to see that the filter is doing what you think.

Try these changes

Make these changes in the files beside this guide. Each one only uses what this lesson has already covered. Tick each one when it works.

Check yourself

Three quick questions on this lesson. Get them all right to finish the lesson and earn a pack.

Loading your answers

Teacher's versionHow to run this lesson, what to check, and any answers. Students do not need this.

How to teach it

  • Do filter in the console before any of this is on a page. [1, 2, 3, 4].filter(function (n) { return n > 2; }) and then ask what the original array is now. Unchanged. That surprises people and it is the whole idea.
  • Then .map() on the same array. Ask what is different about the LENGTH of what came back. Filter can only get shorter. Map is always the same length.
  • Step 3 matters. Draw all twelve creatures with no filtering and stop. A student who cannot see the list yet cannot tell a broken filter from a broken card.
  • Delete the list.textContent = "" line on purpose and type three letters. The page fills with duplicates. Ask what happened before you say. This is the bug they will write themselves.
  • Make them try the search with .toLowerCase() removed. Searching in lower case for a name that starts with a capital finds nothing, and the page looks like the filter is broken rather than the comparison.