How I built a random quotes app with vanilla JavaScript

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.

  1. Fetching and displaying quotes
  2. Avoiding duplicate quotes
  3. Wrapping up

Fetching and displaying quotes

The HTML and CSS

Before we do anything, let’s look at the markup I started with.

<!-- Title and paragraph about the app -->
<h1>Random Ron</h1>
<p>Here's a fun <a href="https://en.wikipedia.org/wiki/Ron_Swanson">Ron Swanson</a> quote 🤭</p>

<!-- Message for those with JavaScript disabled -->
<div id="app">
  <noscript>
    <p>
      <strong>Sorry, you need to enable JavaScript in your browser for this app to work properly. 'Tis impossible without it!</strong>
    </p>
  </noscript>
</div>

<!-- For announcing updates to screen readers -->
<div class="screen-reader" aria-live="polite"></div>

<!-- For showing new quotes -->
<button type="button">Show Another Quote</button>

The most noteworthy part is the .screen-reader element. It’s visually hidden, but still accessible to screen readers. Here’s the CSS that makes this work (borrowed from my friend Chris Ferdinandi, who actually runs the Academy):

.screen-reader {
  border: 0;
  clip: rect(0 0 0 0);
  height: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

The aria-live attribute is even more 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.

There’s a reason why I created an empty div for this rather than just add the aria-live attribute directly to the #app element. The attribute only seems to work when you’re inserting plain text—not HTML. When the time comes to add the quote to the page, I will first update the innerHTML of the #app element, and then the textContent of the .screen-reader element.

This feels like a bit of a hack, so if any accessibility experts have a better solution, please let me know!

Immediately-invoked function expression

As with most of my projects, I started by creating an immediately-invoked function expression (also known as an IIFE). This allowed me to keep my code outside the global scope and minify my references to the document. I also added the "use strict" statement so I could opt into strict mode.

;(function (d) {

  // Opt into strict mode
  "use strict";

})(document);

Polyfills

I planned to use Promises and the Fetch API for this project, both of which are well supported in modern browsers, but not in older versions. To resolve this issue, I added the following script element just before the closing </body> tag in my HTML, and before my own script element:

<script crossorigin="anonymous" src="https://polyfill.io/v3/polyfill.min.js?features=default%2Cfetch"></script>

This adds the default bundle of polyfills from polyfill.io, and one for the Fetch API. A polyfill is a snippet of code that adds support for a feature to browsers that don’t offer it natively (thanks for the succinct definition, Chris!).

The great thing about polyfill.io is that it only returns polyfills to browsers that need them. The latest version of Chrome will get nothing, but older browsers will get the code they need to make the requested APIs work. Neat, right?

For more on polyfills, please check out Everything you ever wanted to know about polyfills on The Vanilla JS Podcast.

Variables

The first thing I did inside my IIFE was create my variables. I added one for the API endpoint (so I could easily change it later), and three for the elements I needed from the DOM:

// Store the API endpoint
var url = "https://ron-swanson-quotes.herokuapp.com/v2/quotes";

// Get the #app, .screen-reader, and button elements
var app = d.querySelector("#app");
var screenReader = d.querySelector(".screen-reader");
var button = d.querySelector("button");

Functions

Next, I created some functions to pass into the then() and catch() methods. These methods get chained onto my call to the fetch() function. They run if the Promise on which they were called was resolved or rejected, respectively.

To learn about the Fetch API, please check out Chris’ post How to use the Fetch API with vanilla JS.

Get the JSON data

Before I could do anything with the Fetch request, I needed to get the data in JSON format. I did that by calling the json() method on the response.

If the request was unsuccessful, though, I needed to reject the Promise. I did that using the static Promise.reject() method, passing in the response.

I used the ternary operator here to make my code more concise. If the response was successful (i.e. its ok property was true), I returned the JSON data; if not, I returned a rejected Promise.

/**
 * Get the JSON data from a Fetch request
 * @param   {Object} response The response to the Fetch request
 * @returns {Object}          The JSON data OR a rejected Promise
 */
function getJSON (response) {
  return response.ok ? response.json() : Promise.reject(response);
}

Insert the quote

If the call to the getJSON() function is successful, it returns an array containing a single Ron Swanson quote as a string. For example:

[ "There are only three ways to motivate people: money, fear, and hunger." ]

I created a function, insertQuote(), to add the quote to the DOM and announce the change to screen readers. The data parameter refers to the JSON data returned by the getJSON() function.

The reason I didn’t need to actually retrieve the quote from the array using data[0] is because the JavaScript interpreter automatically gets all the array items, joins them with a comma as the separator, and then uses this string. There’s only a single item in this array, though, so that’s all that gets used.

This is called type coercion. In hindsight, I think it would have been better to be more explicit and use data[0]. It’s just a better practice because it avoids unexpected behaviour and errors.

/**
 * Add a quote to the DOM
 * @param {Array} data The quote array
 */
function insertQuote (data) {

  // Add the quote to the DOM
  app.innerHTML =
    "<blockquote>" +
      "<p>" + data + "</p>" +
    "</blockquote>";

  // Announce the change to screen readers
  screenReader.textContent = app.textContent;

}

Handling errors

Next up, I created a function, insertError(), to pass into the catch() method. It adds an error message to the DOM if there is a problem anywhere along my chain of Promises.

It’s pretty much indentical to my insertQuote() function. Were I to revisit this project, I would probably refactor these functions into one to make my code more DRY (Don’t Repeat Yourself) and remove a bit of redundancy.

/**
 * Add an error message to the DOM
 */
function insertError () {

  // Add the error message to the DOM
  app.innerHTML =
    "<p>" +
      "Oh no! There was a problem getting the Ron Swanson quote! 😞" +
    "</p>";

  // Announce the change to screen readers
  screenReader.textContent = app.textContent;

}

Wrapping all this into a single function

The last function I created wraps all of this into a single function called fetchQuote(). This was necessary because I wanted to able to run these steps not only when the page loads, but also when the user presses the Show Another Quote button. Using a single function avoids duplicate code.

/**
 * Fetch a quote and add it to the DOM
 */
function fetchQuote () {

  fetch(url)
    .then(getJSON)
    .then(insertQuote)
    .catch(insertError);

}

Inits and event listeners

The final step was to initialise the app and add an event listener to the Show Another Quote button. I simply called my fetchQuote() function on page load and used it as the callback function for my click handler.

// Show a quote when the page loads
fetchQuote();

// Show a quote when the user presses the 'Show Another Quote' button
button.addEventListener("click", fetchQuote);

With that, I had a working app! 🙌

Avoiding duplicate quotes

The problem with the existing solution is that when the user clicks the Show Another Quote button, the API might return the same quote. The change would be too quick for the user to see, so it would look like nothing happened.

To fix this, I modified the project so that if the same quote gets returned from the API in the last 50 quotes, I skip it and fetch another one instead.

Updating the polyfill bundle

I decided to add an additional polyfill for the Array.prototype.includes() method (which otherwise has no IE support). You’ll see why shortly. Here’s the updated polyfill bundle:

<script crossorigin="anonymous" src="https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.includes%2Cfetch%2Cdefault"></script>

I could have used the Array.prototype.indexOf() method instead, which would have removed the need for this polyfill, but I think the includes() method is more readable.

Adding an array of past quotes

Once I added the polyfill, I created a new variable called pastQuotes. I added it to the top of my script with the rest of my variables. It’s just an empty array to which I will add the the most recent 50 quotes.

var pastQuotes = [];

Checking the quote

I then added a function, checkQuote(), to check the validity of the current quote. If it was among the last fifty quotes, I use recursion to get a new one. Otherwise, I add it to the pastQuotes array. I then return it as a resolved Promise using the static Promise.resolve() method.

/**
 * Check the validity of the current quote
 * @param   {Array} data The quote array
 * @returns {String}     The quote (or a new one)
 */
function checkQuote (data) {

  // If the quote was among the last 50, get a new one
  if (pastQuotes.includes(data[0])) {
    return fetch(url).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 new quote
  return Promise.resolve(data[0]);

}

Updating the chain of Promises

The final step was to simply add my new checkQuote() function to the chain of Promises. Here’s the modified fetchQuote() function:

/**
 * Fetch a quote and add it to the DOM
 */
function fetchQuote () {

  fetch(url)
    .then(getJSON)
    .then(checkQuote)
    .then(insertQuote)
    .catch(insertError);

}

Wrapping up

With that, I have a working app that gets random quotes from an API while avoiding duplicates! 💯

If you’d like, you can view the demo on GitHub Pages or check out the source code on GitHub. Everything is available under the MIT License.

If you want to learn how to build cool apps like this, I highly recommend you join the next session of The Vanilla JS Academy. Chris is such a good guy and he will look after you! ❤️


If you have questions, feedback, or any other suggestions, please do email me. I'd love to hear from you!