Build a Quote Generator with TypeScript

Build a Quote Generator with TypeScript

Typescript is a beautiful language, it gives us a lot of confidence as developers, there are loads of awesome content that share Typescript's awesomeness, but today, we are going to take a different route. You want to build little projects with Typescript so you can solidify your knowledge, and that is why we are here right now.

Project Details

Our quote generator is no different from the ones you've probably built with Javascript or other tutorials have covered, our job here today is to replicate our Javascript code in Typescript.

So then, our app will talk to an API to fetch the quote, and then we can render the quote on our beautiful screen.

This is the first on the #JStoTSconversion series I'd be covering here on my blog. So let's get started with what you need to have fun here.

Requirements

  • HTML5
  • CSS3
  • Javascript
  • Typescripts basics

If you have basic knowledge on these then you are good to go. Our next milestone is to get our project setup out of the way.

Install typescript globally with npm i -g typescript

Structure and Initialization

Open your terminal, create a directory in your favourite location and cd into it.

mkdir ts_quote_generator && cd ts_quote_generator

Next, add the tsconfig.json file in the root.

touch tsconfig.json

Fill the new tsconfig.json configuration file with the code snippet below:

{
  "compilerOptions": {
    "sourceMap": true,
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "ES2015"
    ],
    "module": "CommonJS",
    "outDir": "dist/",
    "strict": true
  }
}

We'll add a styles directory with a styles.css file in it and an index.html in the root.

  • styles
    • styles.css
  • index.html

You can get the HTML file content from this gist and the stylesheet from here

Now let's get down to business.

Create an app.ts file in the root of the project, for testing purposes, add this line of code:

console.log("app is connected");

Now open the terminal and run your first tsc build command.

tsc is short for typescript compiler.

Run this command: tsc --build tsconfig.json. You can also run the tsc command without the arguments, like so: tsc. This should generate a new dist/ directory with two files.

directory-structure.png

Run the app and visit the browser console, we should see our message logging there.

browser-console.png

With our typescript compilation working, we will shift attention to fleshing out the app logic.

// app.ts
const quoteContainer = document.getElementById('quote-container');
const loader = document.getElementById('loader');
const quoteText = document.getElementById('quote');
const authorText = document.getElementById('author');
const twitterBtn = document.getElementById('twitter');
const newQuoteBtn = document.getElementById('new-quote');

First, we register our DOM elements into our typescript file and hold them in memory. When making a request to the API for data, we need to show our loading state, we will write two helper functions for that (showContentLoader) and (hideContentLoader);

// app.ts

const showContentLoader = () => {
  if (loader && quoteContainer) {
    loader.hidden = false;
    quoteContainer.hidden = true;
  }
}

const hideContentLoader = () => {
  if (loader && quoteContainer) {
    if (!loader.hidden) {
      quoteContainer.hidden = false;
      loader.hidden = true;
    }
  }
}

In both functions, you'd notice the line if (loader && quoteContainer) {. This is because in our tsconfig.json file we have specified the rule "strict": true, so typescript will fail to build if we don't guard against null values among other things.

But how did we come about the null value?

When we try to get the HTMLElement from the DOM via getElementById() or any other API, there are 2 possible scenarios;

  • The element exists, and returns the corresponding data, or
  • The element is unavailable at the moment and therefore will return null.

When we try to read the value loader.hidden, we could, in fact, be doing null.hidden, this would crash our app because the getElementById() method returns a union of HTMLElement or null. James Henry talks more about this behaviour in his blog.

What have we gained?

Typescript enforces these checks to help us write quality and less buggy code. By checking for the availability of these elements, we save our app from crashing. Cool right? We will continue with this method throughout the code.

The getQuote Function

The getQuote() is responsible for fetching our quotes from the API, we are expecting a response from that request, and hence, we will utilize Typescript's interface to check for our data shape. Let's get the code;

interface QuoteData {
  quoteAuthor: string;
  quoteText: string;
  quoteLink?: string;
  senderLink?: string;
  senderName?: string;
}

// Get quote from API
const getQuote = async () => {
  showContentLoader();
  const proxyUrl = 'https://cors-anywhere.herokuapp.com/'
  const apiUrl = `https://api.forismatic.com/api/1.0/?method=getQuote&lang=en&format=json`;

  try {
    const response = await fetch(proxyUrl + apiUrl);
    const data: QuoteData = await response.json();

    if (authorText && quoteText) {
      // default to annoynmous if there is no author
      data.quoteAuthor === ''
        ? authorText.innerText = 'Anoynmous'
        : authorText.innerText = data.quoteAuthor;

      // Dynamically change text size
      data.quoteText.length > 120
        ? quoteText.classList.add('long-quote')
        : quoteText.classList.remove('long-quote');

      quoteText.innerText = data.quoteText;

      // show quote
      hideContentLoader();
    }
  } catch (error) {
    getQuote();
  }
}

We ensure that the response coming from the API matches our interface shape with this line const data: QuoteData = await response.json();.

Tweet Function

Hook up the tweet function and the getQuote function like so:

// Tweet quote
const tweetQuote = () => {
  if (quoteText && authorText) {
    const quote = quoteText.innerText;
    const author = authorText.innerText;
    const twitterUrl = `https://twitter.com/intent/tweet?text=${quote} - ${author}`;

    window.open(twitterUrl, '_blank');
  }
}

// Hook up the new tweet event
if (newQuoteBtn && twitterBtn) {
  newQuoteBtn.addEventListener('click', getQuote);
  twitterBtn.addEventListener('click', tweetQuote);
}

// OnLoad
getQuote();

That's all, we have added typescript to our little quote generator app. Your whole app.ts should look like this:

const quoteContainer = document.getElementById('quote-container');
const loader = document.getElementById('loader');
const quoteText = document.getElementById('quote');
const authorText = document.getElementById('author');
const twitterBtn = document.getElementById('twitter');
const newQuoteBtn = document.getElementById('new-quote');

interface QuoteData {
  quoteAuthor: string;
  quoteText: string;
  quoteLink?: string;
  senderLink?: string;
  senderName?: string;
}

const showContentLoader = () => {
  if (loader && quoteContainer) {
    loader.hidden = false;
    quoteContainer.hidden = true;
  }
}

const hideContentLoader = () => {
  if (loader && quoteContainer) {
    if (!loader.hidden) {
      quoteContainer.hidden = false;
      loader.hidden = true;
    }
  }
}

// Get quote from API
const getQuote = async () => {
  showContentLoader();
  const proxyUrl = 'https://cors-anywhere.herokuapp.com/'
  const apiUrl = `https://api.forismatic.com/api/1.0/?method=getQuote&lang=en&format=json`;

  try {
    const response = await fetch(proxyUrl + apiUrl);
    const data: QuoteData = await response.json();

    if (authorText && quoteText) {
      // default to annoynmous if there is no author
      data.quoteAuthor === ''
        ? authorText.innerText = 'Anoynmous'
        : authorText.innerText = data.quoteAuthor;

      // Dynamically change text size
      data.quoteText.length > 120
        ? quoteText.classList.add('long-quote')
        : quoteText.classList.remove('long-quote');

      quoteText.innerText = data.quoteText;

      // show quote
      hideContentLoader();
    }
  } catch (error) {
    getQuote();
  }
}

// Tweet quote
const tweetQuote = () => {
  if (quoteText && authorText) {
    const quote = quoteText.innerText;
    const author = authorText.innerText;
    const twitterUrl = `https://twitter.com/intent/tweet?text=${quote} - ${author}`;

    window.open(twitterUrl, '_blank');
  }
}

// Hook up the new tweet event
if (newQuoteBtn && twitterBtn) {
  newQuoteBtn.addEventListener('click', getQuote);
  twitterBtn.addEventListener('click', tweetQuote);
}

// OnLoad
getQuote();

Final Step

To get your new typescript file ready for the browser, open the terminal and run the build command again.

tsc --build tsconfig.json

Todo

You can optimize the getQuote function, it's recursive nature could mean a perpetual loading or crashing of our app if anything happens with the API providers. Set up a mechanism to guard against that. See the GitHub code here

See you in the next #JStoTSConversion.