Advanced forms

Using form text inputs and dropdown lists with 'real-time' event listeners to build dynamic, interactive web pages.

Learning Goals

At the end of this Tutorial, you will be able to:

  • Use a text input with the input event to filter live API data as the user types.
  • Build a <select> dropdown that triggers a Fetch API call when the user makes a selection.

For this Tutorial, in your exercises folder, create a new web page named forms-2.html.

You will also expand on the list-remote.html and list-remote.js files used in the Accessing remote APIs Tutorial.

Introduction

In the Introduction to forms Tutorial, you worked with text, email, password, and number fields, and learned how to validate them on submission and in real time. In this tutorial, you'll explore two more interactive form elements that are fundamental to modern web applications:

  • A text input that filters live API results on every keystroke.
  • A dropdown list that triggers a Fetch API call when the user selects an option.

Each of these connects a form element to a JavaScript event listener — the same fundamental pattern you have already practised.

Filtering with a text input

You have already used the input event to provide real-time validation feedback. This event 'fires' on every keystroke in an input field. Now, you will use this input event to fire a Fetch API request on every keystroke and display matching results dynamically.

The Rick & Morty API

Display the list-remote.html file from the Accessing remote APIs Tutorial and click the Load Rick & Morty button. You should see a list of characters appear, each with their name, status, and image.

screenshot

The endpoint used by the API is as follows:

https://rickandmortyapi.com/api/character

The Rick & Morty API supports searching by character via a name query string parameter. For example, the URL below returns all characters whose name contains "rick":

https://rickandmortyapi.com/api/character/?name=rick

In the address bar of your browser, test the following two URLs to see the results:

https://rickandmortyapi.com/api/character/?name=morty
https://rickandmortyapi.com/api/character/?name=smith

The above URLs return matching characters. Now try the URL below:

https://rickandmortyapi.com/api/character/?name=zzz

The above returns an error object because no characters match "zzz".

{"error":"There is nothing here"}

Your code needs to check for this and handle it gracefully, rather than crashing when it tries to loop through results that don't exist.

First, add this CSS to the <head> of your list-remote.html file:

section#filter-rm {
    max-width: 500px;
    margin: 40px auto;
    padding: 22px 32px;
    background-color: #f9f9f9;
    border: 1px solid #ddd;
    border-radius: 8px;
    display:none;
}

.form-group {
    display: flex;
    flex-direction: column;
    margin-bottom: 20px;
}

.form-group label {
    font-weight: bold;
    margin-bottom: 6px;
    color: #333;
}

.form-group input[type="text"] {
    padding: 10px 12px;
    font-size: 1rem;
    border: 1px solid #ccc;
    border-radius: 5px;
    outline: none;
    max-width: 320px;
    transition: border-color 0.2s ease;
}

.form-group input[type="text"]:focus {
    border-color: #007BFF;
}

.no-results {
    color: #6c757d;
    font-style: italic;
    margin-top: 12px;
}

This hides the Rick & Morty filter by default.

Now, let's add a text input that lets the user search for any character by name.

In your list-remote.html file, add the following HTML under the <h1> heading:

<section id="filter-rm">
    <h2>Filter Rick & Morty characters</h2>
    <div class="form-group">
        <label for="characterSearch">Search characters:</label>
        <input type="text" id="characterSearch" placeholder="e.g., Rick">
    </div>
</section>

And in list-remote.js, update the event listener for the button-container to hide the search option so that it only displays when the Load Rick & Morty button is clicked.

// Event listener on the parent container
document.getElementById("button-container").addEventListener("click", function(e) {
    if (e.target.id === "btn-countries") {
        document.getElementById("filter-rm").style.display = "none";
        fetchCountriesData();
    }
    else if (e.target.id === "btn-users") {
        fetchUsersData();
        document.getElementById("filter-rm").style.display = "none";

    }
    else if (e.target.id === "btn-rm") {
        // Clears the search field
         document.getElementById("characterSearch").value = "";
        // Clear any existing data and show the filter section
        document.getElementById("remote-data-container").innerHTML = "";
        // loads all characters
        fetchRMData(); 
        // Show the filter 
        document.getElementById("filter-rm").style.display = "block";
    }      
});

Adding the filter logic

Your JavaScript file already has two functions to process the Rick & Morty data.

  • Fetch function: This is named fetchRMData() or similar.
  • Display function: This is named displayRMData() or similar.

Paste the following into your list-remote.js file. Notice how the input event connects directly to the Fetch API call:

// -------------------------------------------------------
// Filter Rick & Morty characters by name
// -------------------------------------------------------

const characterSearch = document.getElementById("characterSearch");

// Fire a fetch on every keystroke
characterSearch.addEventListener("input", () => {
    const searchTerm = characterSearch.value.trim();
    // Don't fetch if the field is empty
    if (searchTerm === "") {
        const container = document.getElementById("remote-data-container");
        container.innerHTML = "";
        return;
    }
    fetchRMData(searchTerm);
});

Replace your current fetch function with the following:

async function fetchRMData(name = "") {
    try {
        const url = name
            // name not an empty string (user entered name in search box)
            ? `https://rickandmortyapi.com/api/character/?name=${name}`
            // name is an empty string (no name entered in search box)
            : `https://rickandmortyapi.com/api/character`;
        const response = await fetch(url);
        const data = await response.json();

        if (data.error) {
            const container = document.getElementById("remote-data-container");
            container.innerHTML = `

No characters found matching "${name}".

`; return; } displayRMData(data.results); } catch (error) { const container = document.getElementById("remote-data-container"); container.innerHTML = `

⚠️ Could not load data. Please try again.

`; console.error(error); } }

Save your files and test the search box. As you type, matching characters should appear immediately.

Note the following:

  • Because you are using the input event, the Fetch API fires on every keystroke, but only if the field is not empty.
  • Your fetchRMData() function now includes a default name parameter, which will have a default value of an empty string. This means your fetch function can handle both cases: fetching all characters or fetching characters by name.
  • If name is not an empty string, the fetch request URL ends with ?name=.... Otherwise, the fetch URL returns the default character list.
  • The data.error check handles the case where no characters match — without this, the code would crash trying to call .forEach() on undefined.

Let's add one more feature that deals with situations where users enter disallowed characters in the search field. Update the ternary operator in your fetchRMData() function as follows:

const url = name
// name not an empty string (user entered name in search box)
? `https://rickandmortyapi.com/api/character/?name=${encodeURIComponent(name)}`
// name is an empty string (no name entered in search box)
: `https://rickandmortyapi.com/api/character`;

A user typing "Rick & Morty" would break the URL if we didn't use encodeURIComponent(). Use this anytime you want a user input to become part of a URL.

Try it yourself

The Live Character Count
Add a <span id="charCount"></span> under the Rick & Morty search box. Use the input input event to update this span element with the number of characters currently typed (e.g., "Characters: 5").

The Status Filter
The Rick & Morty API also supports filtering by status:
https://rickandmortyapi.com/api/character/?status=alive.

Add a <select> dropdown next to the search box with options: All, Alive, Dead, Unknown.

When the user changes the dropdown, fetch and display only characters matching that status.

The "Debounce" Feature
Right now, if a user types "Morty" quickly, the code fires 5 separate API requests in under a second.

Use a setTimeout to ensure the API call only fires if the user stops typing for 500ms.

More learning resources

Tutorial Quiz

  Take the test

Tutorial Podcast

Sample AI prompts

What is the difference between the input, blur and change events in JavaScript? When would I use each one?
What does encodeURIComponent() do in JavaScript, and what problems can occur if I include raw user input directly in a URL?"
What is the CSS accent-color property and which form elements does it affect?
What is debouncing in JavaScript, and how do I use setTimeout and clearTimeout to debounce an input event listener?