- Practice using forms to trigger
fetch
requests - Add content to the DOM based on user input
- Override default form behavior using JavaScript
In the previous lab, we accessed a remote API on page load and populated some content. This is a common practice in modern web development — when you visit a website, that site may fetch data from a variety of places right as the page loads. Sites often retrieve data from a backend (like your account info) while also retrieving data from other sources like APIs. From a user's perspective, it all just loads when they visit the site.
Now, we're going to look at a slightly different scenario — retrieving data based on user input.
The underlying code isn't much different than what we've seen. We'll pass a
function into an event listener that, when called, sends a fetch
request, then
does something with the retrieved data.
For this code-along, we'll use JSON server as a mock API and build out a form to get specific data from our API.
Imagine you've been hired to help build the frontend for a movie database company specializing in kids' movies. Some initial work has already been done for you — we have an API with some starter data and some initial HTML, but very little JavaScript.
First, let's get the JSON server up and running in the background. From inside
this assignment's local directory, run json-server --watch db.json
to start
the JSON server. In your browser, you can verify the server is running by
navigating to http://localhost:3000/movies
to see the API data.
Leave the server running for now. Open a second terminal window and navigate to this assignment again. We'll use this second window to open files in your browser or text editor while the server is running.
Open index.html
in your text editor and in the browser (open index.html
for
Mac, explorer.exe index.html
for WSL). With everything set up, we can take a
look at the HTML we currently have.
In index.html
, the movies from our database are currently hard-coded along
with their IDs for our convenience. Below these is a form.
<h2>Movies Database</h2>
<ul>
<li>
<h3>The Brave Little Toaster</h3>
<div>ID: 1</div>
</li>
<li>
<h3>The Princess Bride</h3>
<div>ID: 2</div>
</li>
<li>
<h3>Spirited Away</h3>
<div>ID: 3</div>
</li>
</ul>
<section>
<form>
<label for="searchByID">Search By ID</label>
<input id="searchByID" type="text" placeholder="Enter ID here"/>
<input type="submit" />
</form>
</section>
<section id="movieDetails">
<h4>Title</h4>
<p>Summary</p>
</section>
This form doesn't do much at the moment. In the browser, if we type something in and try to submit, our input just disappears.
Your primary task will be to get this form working. When a user inputs a valid ID, the movie information should appear on the page.
Doing this will involve a few steps:
- Add event listeners to capture form data and override a form's default behavior
- Fetch data based on what the user types into that form
- Display that data on the page
By default, HTML form elements will refresh when a Submit input is clicked. Before we can run the code for fetching data, we need to override this behavior.
In ./src/index.js
, we can do this by adding an event listener. Note that it
already contains one event listener and a callback function, init
:
const init = () => {
}
document.addEventListener('DOMContentLoaded', init);
We want to make sure the JavaScript we write executes when the DOM is fully
loaded. Any code related to DOM manipulation should either go in init
or in a
function called within init
.
In our case, we want to add an event listener to the form
element. We would first
target the DOM element we want:
const inputForm = document.querySelector('form');
Then, we'll need to add an event listener to the form, currently represented by
inputForm
in our code.
Event listeners require two arguments: the type of event, a
string, and the listener, a callback function. In our case, we'll want to pass
in 'submit'
as the type. For the listener, we need to provide a callback
function that will be called to 'handle' the event.
When the event is triggered, the callback function we've provided will execute and an object representing the event will be passed in as an argument. We can expect this to happen and can write a parameter in our code to store the event object in a variable:
inputForm.addEventListener('submit', (event) => {
});
At this point, the form will still refresh automatically, as we haven't done
anything to override that yet. The event
object that gets passed in to our
callback contains a particular method we need in order to override our form's
behavior — preventDefault()
.
const init = () => {
const inputForm = document.querySelector('form');
inputForm.addEventListener('submit', (event) => {
event.preventDefault();
});
}
document.addEventListener('DOMContentLoaded', init);
Calling this inside our callback will stop the page from refreshing and allow us
to do something else instead. We can confirm everything is working by adding a
console.log
in our callback:
inputForm.addEventListener('submit', (event) => {
event.preventDefault();
console.log(event);
});
With dev tools open in the browser, if you enter some text and submit the form,
you should see the event
logged.
There is quite a lot stored on this event
object, but we only need one thing:
if we're fetching data based off a user input, we need to get the value of
whatever the user entered; whatever you just entered into the form.
There are two ways we can get this value:
- The
event
object actually contains the value we need - We can select the specific DOM element and get its value
To get the value from our event
object, we first want to access
event.target
. event.target
returns the DOM element targeted
by our event, a <form>
in our case.
event.target
// => <form>..</form>
event.target
has a property, children
, that returns an
HTMLCollection
containing all the nested elements of the
event.target
element.
event.target.children
// => HTMLCollection(3) [label, input#searchByID, input, searchByID: input#searchByID]
Looking at the form, we can see we want to access the second element:
<form>
<label for="searchByID">Search By ID</label>
<input id="searchByID" type="text" placeholder="Enter ID here"/>
<input type="submit" />
</form>
So we access this element via its index:
event.target.children[1]
// => <input id="searchByID" type="text" placeholder="Enter ID here">
And to get the input value, we use the value
attribute
event.target.children[1].value
// => whatever you typed into the input
We will always need to use event.preventDefault()
to stop the page from
refreshing. However, we don't necessarily need to use the event
to get the
value we need. We can also choose to access the input
element directly.
inputForm.addEventListener('submit', (event) => {
event.preventDefault();
const input = document.querySelector('input#searchByID');
console.log(input.value);
});
Both options work for getting the value we need. For now, we'll use the code above.
With this data, and the default form behavior overridden, we can set up a fetch
request.
Let's first set up the basic shell of our fetch
request. To make sure everything
is working and we can connect to the JSON server, we'll send a basic request to
'http://localhost:3000/movies'
:
const init = () => {
const inputForm = document.querySelector('form');
inputForm.addEventListener('submit', (event) => {
event.preventDefault();
const input = document.querySelector('input#searchByID');
console.log(input.value);
fetch('http://localhost:3000/movies')
.then(response => response.json())
.then(data => {
console.log(data);
// LOG: (3) [{…}, {…}, {…}]
});
});
}
document.addEventListener('DOMContentLoaded', init);
If everything is working, you should see an array of three objects logged in the console using the code above.
Note: For users of the Live Server VSCode extension, if the page is reloading when you initiate the fetch request above, you'll need to set up some additional configuration for Live Server to play nicely with
json-server
. Follow the steps in this gist (you'll only need to do this once), then come back to this lesson.
These three objects represent the three 'records' available from the movies API.
In our example, this is enough for us to move on — we have our user input
accessible in input.value
, and each object in data
has an id
property. We
could now iterate over data
and find a match between input.value
and id
.
However, it isn't usually the case that we want to get all records from an API or server. It would be helpful if we could have the 'server' do that work for us.
JSON Server follows RESTful conventions. As a result of these conventions, we can expect to be able to access specific records directly by providing the appropriate parameter in our request URL.
If you open a new tab in your browser and visit
http://localhost:3000/movies/1
, instead of seeing all three movie objects,
you'll be presented with the object with 1
as its id
:
{
"id": 1,
"title": "The Brave Little Toaster",
"summary": "A group of appliances set off on a journey"
}
Similarly, if we pass this URL into our fetch
request, we'll get this single
object in return.
We need to modify the URL we pass to our fetch
function based on the input
typed into the HTML form. Using interpolation, we can adapt our existing code
to do this:
inputForm.addEventListener('submit', (event) => {
event.preventDefault();
const input = document.querySelector('input#searchByID');
fetch(`http://localhost:3000/movies/${input.value}`)
.then(response => response.json())
.then(data => {
console.log(data);
});
});
Now, if you type a valid ID into the form, a specific movie object will be logged!
Note: What happens when you enter an invalid ID? In the console, you
should see a 404
error. Something to think about as you continue to learn —
what are some ways you might handle an invalid request?
We've captured some user input and used it to customize a fetch request to our
JSON server. The final step in our code-along is to display some of the
retrieved data on the page. In the HTML, we have a section
element with an id,
"movieDetails"
, that contains some filler content.
<section id="movieDetails">
<h4>Title</h4>
<p>Summary</p>
</section>
Let's replace Title
and Summary
with data we retrieved from our server. To
do this, we'll work inside the second then
of our fetch
request. First,
we'll access the DOM and store the two elements in JavaScript
fetch(`http://localhost:3000/movies/${input.value}`)
.then(response => response.json())
.then(data => {
const title = document.querySelector('section#movieDetails h4');
const summary = document.querySelector('section#movieDetails p');
});
Here again, we could access these elements in many ways, this is just one way to
approach it. We could add id
attributes to the h4
and p
tags directly.
Next, we want to change the contents of our title
and summary
elements based
on the retrieved data. We can do this by setting their innerText
values to the
appropriate values in our data:
fetch(`http://localhost:3000/movies/${input.value}`)
.then(response => response.json())
.then(data => {
const title = document.querySelector('section#movieDetails h4');
const summary = document.querySelector('section#movieDetails p');
title.innerText = data.title;
summary.innerText = data.summary;
});
All together, our code looks like this:
const init = () => {
const inputForm = document.querySelector('form')
inputForm.addEventListener('submit', (event) => {
event.preventDefault();
const input = document.querySelector('input#searchByID');
fetch(`http://localhost:3000/movies/${input.value}`)
.then(response => response.json())
.then(data => {
const title = document.querySelector('section#movieDetails h4');
const summary = document.querySelector('section#movieDetails p');
title.innerText = data.title;
summary.innerText = data.summary;
});
});
}
document.addEventListener('DOMContentLoaded', init);
In the browser, if we type 1
into the form, we should see info on the Brave
Little Toaster. Type 2
, and we get The Princess Bride. We're
successfully fetching data on demand!
A core aspect of the modern JavaScript-based web is that web pages can dynamically update their content as a user interacts with it. When a user adds a comment, adds an emoji response, etc., refreshing the page isn't a great experience.
In this lesson, we've gone through the basic mechanisms for providing a better
experience. By capturing user input via event listeners, using fetch
requests,
and DOM manipulation, we can update page content as a user requests it. Although
this won't be the case for all events, we also overrode HTML's default behavior.