Build a Quote Generator with TypeScript
Tags: typescript, javascript, dom, api
Published: 2020-10-09 00:33:05

> This post was originally published here [kodeinstincts.hashnode.dev](https://kodeinstincts.hashnode.dev/build-a-quote-generator-with-typescript-1)

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:

```json
{
  "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](https://gist.github.com/phavor/bd89fcdf3035dc33974bab80b6bd05b2) and the stylesheet from [here](https://gist.github.com/phavor/8d149402b754e9692cd0a01f82eeb8d4)

#### 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](https://cdn.hashnode.com/res/hashnode/image/upload/v1602158243869/oX7L91_E3.png)

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

![browser-console.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1602158597911/c5VrOLx1Y.png)

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

```ts
// 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)`;

```ts
// 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](https://jameshenry.blog/typescript-null-and-undefined-types/) 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;

```ts
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:

```ts
// 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:

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

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](https://github.com/crea8lab/quotifr)

See you in the next `#JStoTSConversion`.