How to Create a Simple Random Quote App with Angular
Tags: angular, typescript, scss, jasmine
Published: 2021-03-02 11:31:50

---
title: How to Create a Simple Random Quote App with Angular
published: true
description: how to build a simple random quote page using Angular
tags: angular, typescript, scss, jasmine
cover_image: https://corscheid.dev/images/fcc-random-quote-machine-angular.png
series: Quotes App
---

Originally posted on [corscheid.dev](https://corscheid.dev/blog/angular-random-quote-app)

## Contents

- [Introduction](#introduction)
- [Tech Stack](#tech-stack)
- [Getting Started](#getting-started)
- [Create New Angular App](#create-new-angular-app)
- [Start the Angular Development Server](#start-the-angular-development-server)
- [Modify Main HTML and CSS](#modify-main-html-and-css)
- [Modify App Component](#modify-app-component)
- [Generate QuoteBox Component](#generate-quotebox-component)
- [Testing](#testing)
  - [Component Testing](#component-testing)
  - [End-to-end (e2e) Testing](#end-to-end-e2e-testing)
- [Final Thoughts](#final-thoughts)

## Introduction

In this post I will show how to build a simple random quote page using [Angular](https://angular.io/). I originally created this app using React for a [freeCodeCamp project](https://www.freecodecamp.org/learn/front-end-libraries/front-end-libraries-projects/build-a-random-quote-machine). The original React code can be found on [my CodePen](https://codepen.io/corscheid/pen/vYyYXBd).

This app will fetch quotes from a GitHub gist by `camperbot`, and display a randomly chosen quote from that list when the page first loads. Then when the user clicks the `New quote` button, a new quote is displayed. There is also a `Tweet` button so that the user can tweet the current quote on Twitter.

## Tech Stack

- [Node.js](https://nodejs.org/)
- [Angular](https://angular.io/)
- [Sass (SCSS)](https://sass-lang.com/)
- [TypeScript](https://www.typescriptlang.org/)

This post assumes some knowledge of HTML, CSS, and TypeScript/JavaScript. The source code for this app is on my
[GitHub](https://github.com/corscheid/fcc-random-quote-machine-angular).

## Getting Started

The first thing to do would be to [install Node.js](https://nodejs.org/en/download/) and [install Git](https://git-scm.com/downloads). Once those are installed, the `npm` (Node Package Manager) command will be available for installing various JavaScript packages. The first one we will install is `@angular/cli`, the Angular Command Line Interface tool. The Angular CLI is a very handy and powerful program which can be used to generate a lot of boilerplate code, from creating a new Angular project to generating new components, modules, and services.

```zsh
npm install -g @angular/cli
```

## Create New Angular App

We'll start by generating a new project called `fcc-random-quote-machine-angular` with the following command:

```zsh
ng new fcc-random-quote-machine-angular
```

This will install some packages and set up a new Angular project with the initial files, directories, and dependencies all in place and ready to go. It even initializes a git repository and makes an initial commit.

## Start the Angular Development Server

Angular CLI includes a `serve` command so that we can preview any edits to the source code in the browser with live hot reloading. This is super convenient. The server does not need to be restarted on every change, and at worst the page might need to be refreshed, but most often it does not even need that. All changes made in this project will happen right before our eyes in the browser.

```zsh
ng serve --open
# or the short version:
ng s -o
```

## Modify Main HTML and CSS

The root template in Angular is `src/index.html`. This is the main outermost template file, and is the place to set up such things in the head such as title, meta tags, stylesheets, as well as link external JavaScript. Replace the generated HTML with the following.

`src/index.html`

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>FreeCodeCamp Random Quote Machine (Angular)</title>
    <base href="/" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
  </head>
  <body>
    <app-root id="root"></app-root>
  </body>
</html>
```

Essentially, just a very basic bare-bones HTML file. Note the `app-root` tag, which is where the Angular application will be inserted in the template.

The global stylesheet is at `src/style.scss`. This is the stylesheet that would apply to the app as a whole. We will use it here to target only elements explicitly written in the `src/index.html` file. Components will get their own separate styles later. I used the following simple styles here. This is also where external stylesheets will be imported at the app level.

```scss
/* Bootstrap 5 */
@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css");
/* Font Awesome */
@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css");
/* Google Fonts */
@import url("https://fonts.googleapis.com/css2?family=Amiri&family=Indie+Flower&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Neucha&display=swap");

$blue: #58f;

html,
body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
}

#root {
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: $blue;
  height: 100%;
  overflow-y: hidden;
}
```

As would be expected for such a basic main HTML template, this is a simple set of styles for our main Sass file.

## Modify App Component

All Angular Components are made up of three files when generated by `ng generate`:

- `*.component.html`: the HTML template defining the UI of the component
- `*.component.css`: the private CSS stylesheet specifically for the component
- `*.component.ts`: the TypeScript file where the class defining the logic goes
- `*.component.spec.ts`: the TypeScript file where the component testing code lives

We'll start by updating the `AppComponent` class. This is the root level Angular component, and in this case, it will be responsible for the logic for fetching the quote data and populating the variables that will be used for the quote box component we will generate later. Notice how every Component in Angular makes use of the `@Component()` decorator, where some metadata is passed in about what the component's tag name in an HTML template will be, which file is the HTML template associated with this component, and which file is the associated stylesheet file. Angular CLI will always set things up so that these are all in separate files.

`src/app/app.component.ts`

```typescript
import { Component, OnInit } from "@angular/core";

interface Quote {
  quote: string;
  author: string;
}

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.scss"],
})
export class AppComponent implements OnInit {
  loading: boolean = true;
  quote!: Quote;
  quoteList!: Quote[];
  tweetURL!: string;
  getNewQuote: () => void = (): void => {
    const idx = Math.floor(Math.random() * this.quoteList.length);
    const newQuote = this.quoteList[idx];
    this.quote = newQuote;
  };

  constructor() {}

  ngOnInit() {
    this.fetchData();
  }

  async fetchData(): Promise<void> {
    const quotesURL =
      "https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json";
    const response = await fetch(quotesURL);
    const quotes = await response.json();
    const idx = Math.floor(Math.random() * quotes.quotes.length);
    const newQuote = quotes.quotes[idx];
    this.quoteList = quotes.quotes;
    this.quote = newQuote;
    this.setTweetURL(newQuote);
    this.loading = false;
  }

  setTweetURL(quote: Quote): void {
    this.tweetURL = `https://twitter.com/intent/tweet?hashtags=quotes&related=freecodecamp&text=${quote.quote} --${quote.author}`;
  }
}
```

The HTML template for this component uses the `*ngIf` directive. In Angular templates, this directive causes the loading text to only be rendered if the `loading` property of the `AppComponent` class is "truthy" (in our case, `true`). This value will be true for the short duration of time while the component is fetching the data. When the `fetchData()` method finishes inside the `ngOnInit()` lifecycle hook, everything is fetched and populated, and the `loading` variable will be set to `false`. After loading, the loading text is replaced with the `app-quote-box` instead.

`src/app/app.component.html`

```html
<div *ngIf="loading; else content"><h1 id="loading">loading...</h1></div>
<ng-template #content>
  <app-quote-box
    [author]="quote.author"
    [quote]="quote.quote"
    [tweetURL]="tweetURL"
    [getNewQuote]="getNewQuote"
  ></app-quote-box>
</ng-template>
```

Note the way attributes are set for the `app-quote-box`. This is similar to how React does props in JSX for nested components. The square brackets represents that this attribute is binding to a class instance variable and the value in quotes are JavaScript expressions, in this case variable values coming from the `AppComponent` class. This is how data is passed from a parent component to a child component in Angular.

The only styles the main app component really needs to be concerned about is the loading text rendered while loading. The rest will be handled by the `QuoteBoxComponent`.

`src/app/app.component.scss`

```scss
$white: #fafafa;

#loading {
  color: $white;
  font-family: "Amiri", serif;
}
```

## Generate QuoteBox Component

Now we go to build the component that will be rendered in this `app-quote-box` area of the app component template. The Angular CLI has a really convenient `ng generate` command that can generate component boilerplate files and code for us, put everything where it needs to go in the project, and even automatically update the App Module declarations to include the newly generated component.

```zsh
ng generate component QuoteBox
# or the short version:
ng g c QuoteBox
```

The `QuoteBoxComponent` will be a super basic component with no methods and only some variables that will be used in the HTML template. It's essentially just a View component responsible for some UI. This reminds me of somewhat of basic React function components that only care about rendering UI given some props. Here, instead of receiving props in the constructor and setting the variables there, we have the Angular `@Input()` decorator handling this.

`src/app/quote-box/quote-box.component.ts`

```typescript
import { Component, Input } from "@angular/core";

@Component({
  selector: "app-quote-box",
  templateUrl: "./quote-box.component.html",
  styleUrls: ["./quote-box.component.scss"],
})
export class QuoteBoxComponent {
  @Input() author!: string;
  @Input() quote!: string;
  @Input() tweetURL!: string;
  @Input() getNewQuote!: () => void;

  constructor() {}
}
```

Angular uses double curly brackets to interpolate variable values into templates when used as HTML tag inner text. Event handlers such as onClick have special syntax, like `(click)` here. This binds the function call expression in the quotes to the onClick event for the button.

`src/app/quote-box/quote-box.component.html`

```html
<div id="quote-box">
  <h1 id="text"><i class="fa fa-quote-left"></i> {{ quote }}</h1>
  <p id="author">- {{ author }}</p>
  <div class="btn-row">
    <button class="btn btn-primary" id="new-quote" (click)="getNewQuote()">
      New quote
    </button>
    <a
      id="tweet-quote"
      href="{{ tweetURL }}"
      target="_top"
      class="btn btn-secondary"
    >
      <i class="fa fa-twitter"></i> Tweet
    </a>
  </div>
</div>
```

The quote box styles apply directly to the elements in the template for this component.

`src/app/quote-box/quote-box.component.scss`

```scss
$white: #fafafa;
$black: #3f3f3f;

#quote-box {
  padding: 2em;
  background-color: $white;
  margin: 20%;
  border-radius: 10px;
  color: $black;

  #text {
    font-family: "Amiri", serif;
  }
  #author {
    font-family: "Neucha", cursive;
    font-size: 2.5em;
  }
  .btn-row {
    display: flex;
    flex-direction: row;
    justify-content: flex-end;

    #tweet-quote {
      margin-left: 1em;
    }
  }
}

@media only screen and (max-width: 480px) {
  #quote-box {
    margin: 0;
    overflow-y: auto;
  }
}
```

## Testing

Angular provides some great tooling out of the box for testing. Projects generated by the CLI come with component tests and end-to-end tests right out of the box.

### Component Testing

Every component generated by Angular CLI comes with a `*.component.spec.ts` file for testing the component via Jasmine. Here are some basic tests for the main app component.

`src/app/app.component.spec.ts`

```typescript
import { TestBed } from "@angular/core/testing";
import { AppComponent } from "./app.component";
import { QuoteBoxComponent } from "./quote-box/quote-box.component";

describe("AppComponent", () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [AppComponent, QuoteBoxComponent],
    }).compileComponents();
  });

  it("should create the app", () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app).toBeTruthy();
  });

  it("should render loading text when loading", () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    fixture.detectChanges();
    const compiled = fixture.nativeElement;
    expect(app.loading).toBeTrue();
    expect(compiled.querySelector("#loading").textContent).toEqual(
      "loading..."
    );
  });

  it("should render QuoteBoxComponent after loading", async () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    await app.fetchData();
    fixture.detectChanges();
    const compiled = fixture.nativeElement;
    expect(app.loading).toBeFalse();
    expect(compiled.querySelector("app-root app-quote-box")).toBeDefined();
  });
});
```

And for the quote box component, only a simple existence test:

`src/app/quote-box/quote-box.component.spec.ts`

```typescript
import { ComponentFixture, TestBed } from "@angular/core/testing";

import { QuoteBoxComponent } from "./quote-box.component";

describe("QuoteBoxComponent", () => {
  let component: QuoteBoxComponent;
  let fixture: ComponentFixture<QuoteBoxComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [QuoteBoxComponent],
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(QuoteBoxComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it("should create", () => {
    expect(component).toBeTruthy();
  });
});
```

Run the following command to execute all the component tests.

```zsh
ng test
```

### End-to-end (e2e) Testing

Angular also has end-to-end (e2e) testing in every project out of the box as well. Rather than including the external freeCodeCamp testing JavaScript as a script tag in the main `index.html` file, I thought it would be nice to rewrite them as e2e tests. We'll modify the following two files:

- `e2e/src/app.e2e-spec.ts`
- `e2e/src/app.po.ts`

The first of those files contains the test suite code and the second one contains a sort of page utility class used in the test suite, to keep things a little more organized.

`e2e/src/app.e2e-spec.ts`

```typescript
import { browser, logging } from "protractor";
import { AppPage } from "./app.po";

describe("workspace-project App", () => {
  describe("Content", () => {
    let page: AppPage;

    beforeEach(() => {
      page = new AppPage();
    });

    it("should display quote box", async () => {
      await page.navigateTo();
      expect(await page.getQuoteBox()).toBeTruthy();
    });

    it("should display text element inside quote box with random quote", async () => {
      expect(await page.getQuoteBoxText()).toBeTruthy();
    });

    it(`should display author element inside quote box with quote's author`, async () => {
      expect(await page.getQuoteBoxAuthor()).toBeTruthy();
    });

    it('should display "New quote" button inside quote box', async () => {
      expect(await page.getNewQuoteButtonText()).toEqual("New quote");
    });

    it('should display "Tweet" button inside quote box', async () => {
      expect(await page.getTweetButtonText()).toEqual("Tweet");
    });

    it('should fetch new quote when "New quote" button is clicked', async () => {
      const initialQuoteText = await page.getQuoteBoxText();
      await page.clickQuoteButton();
      const newQuoteText = await page.getQuoteBoxText();
      expect(initialQuoteText).toBeTruthy();
      expect(newQuoteText).toBeTruthy();
      expect(newQuoteText).not.toEqual(initialQuoteText);
    });

    it(`should update new quote's author when "New quote" button is clicked`, async () => {
      const initialAuthor = await page.getQuoteBoxAuthor();
      await page.clickQuoteButton();
      const newAuthor = await page.getQuoteBoxAuthor();
      expect(initialAuthor).toBeTruthy();
      expect(newAuthor).toBeTruthy();
      expect(newAuthor).not.toEqual(initialAuthor);
    });

    it('should open Twitter tweet intent when "Tweet" button is clicked', async () => {
      expect(await page.getTweetURL()).toMatch(
        /^https:\/\/twitter\.com\/intent\/tweet/
      );
    });

    afterEach(async () => {
      // Assert that there are no errors emitted from the browser
      const logs = await browser.manage().logs().get(logging.Type.BROWSER);
      expect(logs).not.toContain(
        jasmine.objectContaining({
          level: logging.Level.SEVERE,
        } as logging.Entry)
      );
    });
  }),
    describe("Layout", () => {
      let page: AppPage;

      beforeEach(() => {
        page = new AppPage();
      });

      it("should display the quote box in the center horizontally", async () => {
        const htmlElementBounds = await page.getHtmlElementBounds();
        const quoteBoxBounds = await page.getQuoteBoxBounds();
        const left = quoteBoxBounds.x0 - htmlElementBounds.x0;
        const right = htmlElementBounds.x1 - quoteBoxBounds.x1;
        expect(Math.abs(left - right)).toBeLessThan(20);
      });

      afterEach(async () => {
        // Assert that there are no errors emitted from the browser
        const logs = await browser.manage().logs().get(logging.Type.BROWSER);
        expect(logs).not.toContain(
          jasmine.objectContaining({
            level: logging.Level.SEVERE,
          } as logging.Entry)
        );
      });
    });
});
```

`e2e/src/app.po.ts`

```typescript
import { browser, by, element, ElementFinder } from "protractor";

interface ISize {
  width: number;
  height: number;
}

interface ILocation {
  x: number;
  y: number;
}

interface ElementXPair {
  x0: number;
  x1: number;
}

export class AppPage {
  async navigateTo(): Promise<unknown> {
    return browser.get(browser.baseUrl);
  }

  async getQuoteBox(): Promise<string> {
    let quoteBox: ElementFinder = element(
      by.css("app-root app-quote-box #quote-box")
    );
    let quoteBoxContent: string = await quoteBox.getText();
    return quoteBoxContent;
  }

  async getQuoteBoxText(): Promise<string> {
    let quoteBoxText: ElementFinder = element(
      by.css("app-root app-quote-box #quote-box #text")
    );
    let quoteBoxTextContent: string = await quoteBoxText.getText();
    return quoteBoxTextContent;
  }

  async getQuoteBoxAuthor(): Promise<string> {
    let quoteBoxAuthor: ElementFinder = element(
      by.css("app-root app-quote-box #quote-box #author")
    );
    let quoteBoxAuthorContent: string = await quoteBoxAuthor.getText();
    return quoteBoxAuthorContent;
  }

  async getNewQuoteButtonText(): Promise<string> {
    let newQuoteButton: ElementFinder = element(
      by.css("app-root app-quote-box #quote-box .btn-row #new-quote")
    );
    let newQuoteButtonText: string = await newQuoteButton.getText();
    return newQuoteButtonText;
  }

  async getTweetButtonText(): Promise<string> {
    let tweetButton: ElementFinder = element(
      by.css("app-root app-quote-box #quote-box .btn-row #tweet-quote")
    );
    let tweetButtonText: string = await tweetButton.getText();
    return tweetButtonText;
  }

  async clickQuoteButton(): Promise<void> {
    let newQuoteButton: ElementFinder = element(
      by.css("app-root app-quote-box #quote-box .btn-row #new-quote")
    );
    await newQuoteButton.click();
  }

  async clickTweetButton(): Promise<void> {
    let tweetButton: ElementFinder = element(
      by.css("app-root app-quote-box #quote-box .btn-row #tweet-quote")
    );
    await tweetButton.click();
  }

  async getTweetURL(): Promise<string> {
    let tweetButton: ElementFinder = element(
      by.css("app-root app-quote-box #quote-box .btn-row #tweet-quote")
    );
    let tweetButtonURL = await tweetButton.getAttribute("href");
    return tweetButtonURL;
  }

  async getHtmlElementBounds(): Promise<ElementXPair> {
    let htmlElement: ElementFinder = element(by.tagName("html"));
    let htmlElementSize: ISize = await htmlElement.getSize();
    let htmlElementLocation: ILocation = await htmlElement.getLocation();
    let htmlElementBounds: ElementXPair = {
      x0: htmlElementLocation.x,
      x1: htmlElementLocation.x + htmlElementSize.width,
    };
    return htmlElementBounds;
  }

  async getQuoteBoxBounds(): Promise<ElementXPair> {
    let quoteBox: ElementFinder = element(
      by.css("app-root app-quote-box #quote-box")
    );
    let quoteBoxSize: ISize = await quoteBox.getSize();
    let quoteBoxLocation: ILocation = await quoteBox.getLocation();
    let quoteBoxBounds: ElementXPair = {
      x0: quoteBoxLocation.x,
      x1: quoteBoxLocation.x + quoteBoxSize.width,
    };
    return quoteBoxBounds;
  }
}
```

This one has all the methods used for getting certain text and other things from elements on the DOM.

To run all the e2e tests, run the following command. (make sure to quit the `ng serve` command first, to free up port 4200)

```zsh
ng e2e
```

This will open an automated instance of Chrome as it runs through the UI tests. Test results will be logged to the terminal.

## Final Thoughts

I think this was an interesting little project for playing around with some basic Angular components, templates, directives, etc. In the next post, We'll compare and contrast the React code and Angular code for the same app. React and Angular are similar in that they are component-based, but take slightly different approaches to the same problem of creating the front-end of single-page applications.