Celebrate Bisexuality Day

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);

View demo on CodePen

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);

View demo on CodePen

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! ❤️