My third challenge for the Vanilla JS Academy was to build a random quotes app using an API. Let's look at how I approached the task.
Before we do anything, let's look at the markup I started with:
<blockquote>
<p id="quote" aria-live="polite">Loading...</p>
</blockquote>
<button id="button" type="button">Show Another Quote</button>
The aria-live
attribute is important. It informs screen readers that the content of the element will change, and that they should therefore pay attention to it and announce the changes.
Fetching and displaying quotes
First up, I created three variables: one for the #quote
element, one for the #button
element, and one for the API endpoint:
// Get the placeholder for the quote
var quote = document.querySelector("#quote");
// Get the button for fetching new quotes
var button = document.querySelector("#button");
// Store the API endpoint
var endpoint = "https://ron-swanson-quotes.herokuapp.com/v2/quotes";
Next, I added in my getJSON()
helper function:
/**
* Get the JSON data from a Fetch request
* @param {Object} response The response to the request
* @returns {Object} The JSON data OR a rejected promise
*/
function getJSON (response) {
return response.ok ? response.json() : Promise.reject(response);
}
This allowed me to get the JSON data from the API and log it to the console:
fetch(endpoint)
.then(getJSON)
.then(console.log)
.catch(console.error);
By default, the API returns an array containing one random quote:
["Creativity is for people with glasses who like to lie."]
I needed to add this quote to the DOM, so I created a function to handle that:
/**
* Show a new quote on the page
* @param {Array} quotes An array of quotes returned by the API
*/
function showQuote (quotes) {
quote.textContent = quotes[0];
}
Then I added this function to my chain of promises:
fetch(endpoint)
.then(getJSON)
.then(showQuote)
.catch(console.error);
I wanted to actually show an error message if there was a problem, though, rather than just log it to the console. So I created a function for that as well. It replaces the blockquote
element with a paragraph:
/**
* Show an error on the page
*/
function showError () {
quote.parentNode.outerHTML = (
"<p>" +
"<strong>" +
"Sorry, there was a problem getting your Ron Swanson quote! Please try again later." +
"</strong>" +
"</p>"
);
}
Again, I added this to the chain of promises:
fetch(endpoint)
.then(getJSON)
.then(showQuote)
.catch(showError);
Now I could see a random quote when the page loaded, but I also wanted the #button
element to show a new quote on click. So the next thing I did was wrap my promise chain inside its own function:
/**
* Fetch a new quote and show it on the page
*/
function fetchQuote () {
fetch(endpoint)
.then(getJSON)
.then(showQuote)
.catch(showError);
}
Finally, to initialize my script, I called this function on page load and also inside a click handler for the #button
element:
// Show a quote when the page loads
fetchQuote();
// Show a new quote when the user clicks the button
button.addEventListener("click", fetchQuote);
Avoiding duplicate quotes
With the existing code, it was possible for the same quote to be used twice in a row. To the user, it would have looked like nothing happened.
To fix this, I added a new variable that I would use to store the last 50 quotes:
// Store the past 50 quotes
var pastQuotes = [];
Then I added a function to check if the current quote is valid. If the quote was among the last 50, the function recursively calls itself until it finds a new one:
/**
* If the quote was among the last 50, get a new one
* @param {Array} data The array of quotes returned by the API
* @returns {String} The fetched quote (if valid) OR a new quote
*/
function checkQuote (data) {
// If the quote was among the last 50, get a new one
if (pastQuotes.includes(data[0])) {
return fetch(endpoint).then(getJSON).then(checkQuote);
}
// If the pastQuotes array has 50 quotes, reset it
if (pastQuotes.length === 50) {
pastQuotes = [];
}
// Add this quote to the pastQuotes array
pastQuotes.push(data[0]);
// Return the quote
return data[0];
}
I also renamed the parameter of my showQuote()
function from quote
to quoteString
:
/**
* Show a new quote on the page
* @param {String} quoteString The quote returned by the API
*/
function showQuote (quoteString) {
quote.textContent = quoteString;
}
This was necessary because this function would now receive a string as an argument, not an array of quotes.
I couldn't just call it quote
because that would have caused problems; I needed to access the quote
variable outside this function's scope. If I had called the parameter quote
as well, the code would have referenced that local variable instead.
Finally, I updated my chain of promises to include the new checkQuote()
function:
fetch(endpoint)
.then(getJSON)
.then(checkQuote)
.then(showQuote)
.catch(showError);
If you want to build cool projects like this, I strongly encourage you to sign up for the next session of the Vanilla JS Academy! ❤️