developers

RxJS Advanced Tutorial: Use the Madlibs API

Build an app to learn about the power and flexibility of RxJS in Angular while exploring speech recognition with Web Speech API.

Sep 20, 201727 min read

In this tutorial series, we'll learn how to build a small app with some big concepts. We'll cover reactive programming with Reactive Extensions (Rx*), JS framework component interaction in Angular, and speech recognition with the Web Speech API. The completed Madlibs app code can be found at this GitHub repo.


In this part, we will cover:

  • Keyboard component fallback if user's browser does not support speech recognition
  • Generating words by fetching them from an API
  • RxJS operators to work with API requests
  • Creating an observable for a Progress Bar component
  • Displaying the user's madlib story
  • Aside: authentication of an Angular app and Node API with Auth0

Let's pick up right where we left off.

Angular App Keyboard Component

Not all browsers support speech recognition. If we view our app right now in a browser that doesn't, we'll see nothing but a header. An app that doesn't work in most browsers isn't useful. Let's make a Keyboard component to use in place of the Listen component so users with unsupported browsers can still create a fun madlib using the Words Form component.

An app that doesn't work in most browsers isn't useful: implement fallbacks for browsers that don't support features.

Tweet This

Create another new component with the Angular CLI like so:

$ ng g component keyboard

Keyboard Component Class

Open the

keyboard.component.ts
file and implement the following code:

// src/app/keyboard/keyboard.component.ts
import { Component } from '@angular/core';
import { Words } from './../words';

@Component({
  selector: 'app-keyboard',
  templateUrl: './keyboard.component.html',
  styleUrls: ['./keyboard.component.scss']
})
export class KeyboardComponent {
  nouns: string[] = new Words().array;
  verbs: string[] = new Words().array;
  adjs: string[] = new Words().array;

  constructor() { }

}

This is a very simple component that primarily consists of a template. It is, however, still going to be a parent component (with Words Form as a child), so we'll import the

Words
class and set up the
nouns
,
verbs
, and
adjs
arrays like we did in the Listen component. We can then pass these as inputs to the Words Form component.

Keyboard Component Template

Let's open the

keyboard.component.html
file and add our template:

<!-- src/app/keyboard/keyboard.component.html -->
<div class="alert alert-info mt-3">
  <h2 class="text-center mt-3">Type Words to Play</h2>
  <p>You may enter your own madlib words in the fields below. Here are some examples:</p>
  <ul>
    <li><strong>Noun:</strong> <em>"cat"</em> (person, place, or thing)</li>
    <li><strong>Verb:</strong> <em>"jumping"</em> (action, present tense), <em>"ran"</em> (action, past tense)</li>
    <li><strong>Adjective:</strong> <em>"flashy"</em> (describing word)</li>
  </ul>
</div>
<app-words-form
  [nouns]="nouns"
  [verbs]="verbs"
  [adjs]="adjs"></app-words-form>

This simply adds some instructions similar to the Listen component and displays the

WordsForm
component.

Add Keyboard Component to App Component

Now let's display the Keyboard component instead of the Listen component if speech is not supported. Open the

app.component.html
template and make the following addition:

<!-- src/app/app.component.html -->
<div class="container">
  <h1 class="text-center">Madlibs</h1>
  <app-listen *ngIf="speech.speechSupported"></app-listen>
  <app-keyboard *ngIf="!speech.speechSupported"></app-keyboard>
</div>

Now if speech recognition is not supported, the user will see the Keyboard component and can still enter words manually with the form. The app should look like this in a browser that doesn't support speech recognition:

Angular RxJS speech not supported Madlibs app

Generate Words With Madlibs API

Now that we can speak and type words to generate a madlib, there's one more method we want to offer to users to create their custom story: automatic word generation using a prebuilt Node API.

Set Up Madlibs API

Clone the madlibs-api locally to a folder of your choosing. Then open a command prompt or terminal window in that folder and run the following commands:

$ npm install
$ node server

This will install the required dependencies and then run the API on

localhost:8084
. You should be able to visit the API in the browser at http://localhost:8084/api to confirm it's working properly. You can then access its endpoints. Check out the repository's README to see all the available endpoints. You can try them out in the browser to see what they return (for example, http://localhost:8084/api/noun). Take a minute to become familiar with the API and its endpoints and give some thought to how we might leverage the API in our application to generate arrays of the different parts of speech.

Intended Functionality to Generate Words With API

Now that you have the madlibs API set up and running and you've familiarized yourself with how it works, let's consider what our intended functionality is.

Recall the specifics of what we're expecting the user to respond with based on our word arrays and the Words Form placeholders. In order to generate the appropriate words automatically, we'll need the following from the API:

  • An array containing 5 nouns: 1 person, 2 places, and 2 things
  • An array containing 5 verbs: 2 present tense and 3 past tense
  • An array containing 5 adjectives

The API, however, does not return arrays, it returns single text strings. Also, we have different endpoints for people, places, things, present and past tenses, etc. How can we reconcile the API functionality with our requirements?

Luckily, we have RxJS available to us! Let's explore how we can use this powerful library to get exactly what we want from the API.

Add HTTP to App Module

The first thing we need to do is add Angular HTTP to our App module. Open the

app.module.ts
file and add:

// src/app/app.module.ts
...
import { HttpClientModule } from '@angular/common/http';
...

@NgModule({
  ...,
  imports: [
    ...,
    HttpClientModule
  ],
  ...

We'll import the

HttpClientModule
and add it to the NgModule's
imports
array, making the module available to our application.

Add HTTP Requests to Madlibs Service

Now open the

madlibs.service.ts
file. We'll add our HTTP requests to this file.

// src/app/madlibs.service.ts
...
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/zip';
import 'rxjs/add/observable/forkJoin';

@Injectable()
export class MadlibsService {
  ...
  private _API = 'http://localhost:8084/api/';

  constructor(private http: HttpClient) { }

  ...

  private _stringSuccessHandler(res: string): string {
    // Remove all double quotes from response
    // This is a product of receiving text response
    return res.replace(/"/g, '');
  }

  private _errorHandler(err: HttpErrorResponse | any) {
    const errorMsg = err.message || 'Error: Unable to complete request.';
    return Observable.throw(errorMsg);
  }  

  getNouns$() {
    const nounPerson$ = this.http
      .get(`${this._API}noun/person`, {responseType: 'text'})
      .map(this._stringSuccessHandler)
      .catch(this._errorHandler);

    const nounPlace$ = this.http
      .get(`${this._API}noun/place`, {responseType: 'text'})
      .map(this._stringSuccessHandler)
      .catch(this._errorHandler);

    const nounThing$ = this.http
      .get(`${this._API}noun/thing`, {responseType: 'text'})
      .map(this._stringSuccessHandler)
      .catch(this._errorHandler);

    return Observable.forkJoin([nounPerson$, nounPlace$, nounPlace$, nounThing$, nounThing$]);
  }

  getVerbs$() {
    const verbPresent$ = this.http
      .get(`${this._API}verb/present`, {responseType: 'text'})
      .map(this._stringSuccessHandler)
      .catch(this._errorHandler);

    const verbPast$ = this.http
      .get(`${this._API}verb/past`, {responseType: 'text'})
      .map(this._stringSuccessHandler)
      .catch(this._errorHandler);

    return Observable.forkJoin([verbPresent$, verbPresent$, verbPast$, verbPast$, verbPast$]);
  }

  getAdjs$() {
    const adj$ = this.http
      .get(`${this._API}adjective`, {responseType: 'text'})
      .map(this._stringSuccessHandler)
      .catch(this._errorHandler);

    return Observable.forkJoin([adj$, adj$, adj$, adj$, adj$]);
  }

  getWords$() {
    return Observable
      .zip(this.getNouns$(), this.getVerbs$(), this.getAdjs$())
      .map((res) => {
        return {
          nouns: res[0],
          verbs: res[1],
          adjs: res[2]
        };
      });
  }

}

Let's go over these additions step by step. First we'll add some new imports:

HttpClient
and
HttpErrorResponse
from Angular common,
Observable
from RxJS, and the
map
and
catch
RxJS operators.

Next we'll add some new properties:

_API
to store the API URI and a
words
object to store the words retrieved from the API.

We'll make

HttpClient
available to our component in the constructor.

Next we have our private

_stringSuccessHandler()
. This method takes a text HTTP response and strips the double quotes (
"
) that are automatically added to it, leaving just the word. We'll use this success handler with all of our API requests that return a text response.

The private

_errorHandler()
method cancels the observable with an error message in case something went wrong with the request.

Combining Observables With ForkJoin

Next, let's take a closer look at the

getNouns$()
method (and the two methods that follow it,
getVerbs$()
and
getAdjs$()
).

  getNouns$() {
    const nounPerson$ = this.http
      .get(`${this._API}noun/person`, {responseType: 'text'})
      .map(this._stringSuccessHandler)
      .catch(this._errorHandler);

    const nounPlace$ = this.http
      .get(`${this._API}noun/place`, {responseType: 'text'})
      .map(this._stringSuccessHandler)
      .catch(this._errorHandler);

    const nounThing$ = this.http
      .get(`${this._API}noun/thing`, {responseType: 'text'})
      .map(this._stringSuccessHandler)
      .catch(this._errorHandler);

    return Observable.forkJoin([nounPerson$, nounPlace$, nounPlace$, nounThing$, nounThing$]);
  }

Notice that there are three HTTP requests declared as constants in the

getNouns$()
function: one each for retrieving a person, place, and thing. Each
GET
request expects a
responseType: 'text'
. Each text request is then
map
ped with our
_stringSuccessHandler
and has a
catch
method with our
_errorHandler
in case something went wrong. Recall that our app expects an array of nouns that includes one person, two places, and two things. Now that we have
nounPerson$
,
nounPlace$
, and
nounThing$
observables set up, we can use the RxJS combination operator
forkJoin
to execute all observables at the same time and then emit the last value from each one once they are all complete.

We'll

return Observable.forkJoin()
, passing in the array of observables we'd like to use in the expected order. If all requests are successful, subscribing to this observable will produce an array that looks like this:

[person, place, place, thing, thing]

We'll use

forkJoin
again with our
getVerbs$()
method to return a stream that emits an array of verbs in the following tenses:

[present, present, past, past, past]

To get adjectives, we'll

forkJoin
an adjective request five times, since all adjectives are the same, but each one requires its own request. This will result in:

[adjective, adjective, adjective, adjective, adjective]

Combining Observables With Zip

Now we have observables for each of the three parts of speech our app expects:

getNouns$()
,
getVerbs$()
, and
getAdjs$()
. However, these are still separate streams. Ultimately, we don't want to subscribe to three different observables and wait for each to emit independently.

Thanks again to RxJS, we can combine all three streams into a single observable:

getWords$()
. The
zip
combination operator
allows us to pass multiple observables as arguments. After all the observables have emitted, the zipped observable emits the results in an array.

  getWords$() {
    return Observable
      .zip(this.getNouns$(), this.getVerbs$(), this.getAdjs$())
      .map((res) => {
        return {
          nouns: res[0],
          verbs: res[1],
          adjs: res[2]
        };
      });
  }

Once we receive the resulting array that includes the zipped arrays from the nouns, verbs, and adjectives observables, we'll

map
the response to an easy-to-read object.

We can now subscribe to the

getWords$()
observable in components and receive an object that looks like this:

{
  nouns: [person, place, place, thing, thing],
  verbs: [present, present, past, past, past],
  adjs: [adjective, adjective, adjective, adjective, adjective]
}

This is exactly what we want from the API when generating words, and it's easy to accomplish, thanks to RxJS.

RxJS operators like forkJoin and zip make it simple to combine HTTP request observables.

Tweet This

Generate Words Component

Now that we've implemented the necessary methods to get nouns, verbs, and adjectives from the API, it's time to put them to use in our application.

Let's create a Generate Words component. This component will use the Madlibs service to set up a subscription to fetch the words from the API when the user clicks a button. It will then emit an event containing the API data so that other components (such as the Words Form component) can use that data.

Generate the new component:

$ ng g component generate-words

Generate Words Component Class

Open the

generate-words.component.ts
file and add the following code:

// src/app/generate-words/generate-words.component.ts
import { Component, Output, OnDestroy, EventEmitter } from '@angular/core';
import { MadlibsService } from './../madlibs.service';
import { Subscription } from 'rxjs/Subscription';

@Component({
  selector: 'app-generate-words',
  templateUrl: './generate-words.component.html',
  styleUrls: ['./generate-words.component.scss']
})
export class GenerateWordsComponent implements OnDestroy {
  @Output() fetchedWords = new EventEmitter;
  wordsSub: Subscription;
  loading = false;
  generated = false;
  error = false;

  constructor(private ml: MadlibsService) { }

  fetchWords() {
    this.loading = true;
    this.generated = false;
    this.error = false;

    this.wordsSub = this.ml.getWords$()
      .subscribe(
        (res) => {
          this.loading = false;
          this.generated = true;
          this.error = false;
          this.fetchedWords.emit(res);
        },
        (err) => {
          this.loading = false;
          this.generated = false;
          this.error = true;
          console.warn(err);
        }
      );
  }

  ngOnDestroy() {
    if (this.wordsSub) {
      this.wordsSub.unsubscribe();
    }
  }
}

First we'll add some imports. The

OnDestroy
lifecycle hook is necessary to clean up subscriptions when the component is destroyed.
Output
and
EventEmitter
are needed to emit an event from this component to a parent. Then we'll import our
MadlibsService
to get API data, as well as
Subscription
from RxJS
.

We'll

implement
the
OnDestroy
lifecycle hook when we export our
GenerateWordsComponent
class.

We'll set up an

@Output() fetchedWords = new EventEmitter
property for a parent component to listen for a child event. We also need a
wordsSub
subscription to the
getWords$()
observable, and three boolean properties so the UI can reflect the appropriate states of the app:
loading
,
generated
, and
error
.

We'll make

MadlibsService
available to the component in the constructor function.

The

fetchWords()
method will be executed when the user clicks a button to generate words via the API. When run, this function should indicate that the app is
loading
, words have not yet been
generated
, and there are currently no
error
s.

The

wordsSub
subscription should be set up as well. This subscribes to the
getWords$()
observable from the Madlibs service. On successful response, it updates the app states and emits the
fetchedWords
event with a payload containing the API response. If you recall from our code above, this is an object containing the noun, verb, and adjective arrays. If an error occurs, the app states reflect this and a warning is raised in the console.

Finally, in the

ngOnDestroy()
lifecycle function, we'll check to see if the
wordsSub
exists and
unsubscribe()
from it if so.

Generate Words Component Template

Now open the

generate-words.component.html
template:

<!-- src/app/generate-words/generate-words.component.html -->
<h2 class="text-center mt-3">Generate Words</h2>
<p>You may choose to generate all the necessary madlib words randomly. Doing so will replace any words you may have previously entered.</p>
<p>
  <button
    class="btn btn-primary btn-block"
    (click)="fetchWords()">
      <ng-template [ngIf]="!loading">Generate Words</ng-template>
      <ng-template [ngIf]="loading">Generating...</ng-template>
  </button>
</p>
<p *ngIf="generated" class="alert alert-success">
  <strong>Success!</strong> Madlib words have been generated. Please scroll down to view or edit your words.
</p>
<p *ngIf="error" class="alert alert-danger">
  <strong>Oops!</strong> An error occurred while trying to automatically generate words. Please try again or enter your own words!
</p>

This is a straightforward template that displays some copy informing the user that they can generate words from the API, but doing so will replace any words they may have already entered using speech recognition or the form.

It shows a button that executes the

fetchWords()
method when clicked and changes label depending on the
loading
state.

If data is successfully

generated
using the API, a "Success!" message is shown. Simultaneously (and behind the scenes), an event is emitted with the API data. If an
error
occurred, an error message is displayed.

Update Listen and Keyboard Components

We now need to add our Generate Words component to both the Listen and Keyboard Components. We'll also need to add a little bit of functionality to these components so they can listen for the

fetchedWords
event and react to it by updating their local property data with the words from the API.

Update Listen and Keyboard Component Classes

Open the

listen.component.ts
and
keyboard.component.ts
component classes and add the following method to each file:

// src/app/listen/listen.component.ts
// src/app/keyboard/keyboard.component.ts
...
  onFetchedAPIWords(e) {
    this.nouns = e.nouns;
    this.verbs = e.verbs;
    this.adjs = e.adjs;
  }
  ...

This is the handler for the

fetchedWords
event. It takes the event payload and uses it to define the values of the local
nouns
,
verbs
, and
adjs
properties, thus updating all of these arrays to the data from the API.

Update Listen Component Template

Open the

listen.component.html
template and at the top, add the
<app-generate-words>
element right inside the opening
<div>
:

<!-- src/app/listen/listen.component.html -->
<div class="alert alert-info mt-3">
  <app-generate-words
    (fetchedWords)="onFetchedAPIWords($event)"></app-generate-words>
  ...

This element listens for the

(fetchedWords)
event and runs the
onFetchedAPIWords($event)
handler when the event is emitted, sending the
$event
data as a parameter.

If speech recognition is supported, the app should now look like this in the browser:

Madlibs app with Generate Words component - speech recognition supported

Update Keyboard Component Template

Now open the

keyboard.component.html
template. Below the unordered list in the keyboard instructions, add the Generate Words component like so:

<!-- src/app/keyboard/keyboard.component.html -->
...
  </ul>
  <app-generate-words
    (fetchedWords)="onFetchedAPIWords($event)"></app-generate-words>
</div>
...

Now both the Listen and Keyboard components support word generation with the API. Make sure the API is running locally.

If speech recognition is not supported, the app should now look like this in the browser:

Madlibs RxJS Angular app with Generate Words component - no speech recognition

Playing With the API

You (or the user) should now be able to click the "Generate Words" button whether speech recognition is supported or not. The form should populate with words retrieved from the API. If the user clicks the button again, new random words should be fetched and will overwrite any existing words.

In browsers that support speech recognition, the user should be able to delete API-generated words and then use the speech commands to fill them back in. In any browser, the user can edit or replace words manually by typing in the form.

Progress Bar Component

The next component we're going to add is a bit of flair. It's a progress bar that we'll build with RxJS. Although it won't represent the app actually generating the madlib story (because that happens so quickly a progress indicator would be essentially pointless), it does lend a nice visual and helps us explore another feature of RxJS: creating timer observables.

We'll also call the API and fetch a pronoun while the progress bar is running, but again, with a server running on localhost, this happens so quickly the UI progress bar won't represent the API request and response speed.

Note: If you deploy your Madlibs app to a server, a great exercise would be to modify the progress bar so that it does actually represent the API request in some way.

The progress bar will appear after the user clicks the "Go!" button to generate their madlib. When we're finished, it will look like this in the browser:

Madlibs Angular RxJS app progress bar

Let's go over the features of our Progress Bar component:

  • Subscribe to the Madlibs service's
    submit$
    subject.
  • Replace the submit ("Go!") button with a progress bar.
  • Have a timer observable.
  • Make an API request for a pronoun which should be stored in the Madlibs service so other components can make use of it.
  • The timer observable's subscription should increment the UI to show a progress bar filling up.
  • Once the progress bar reaches completion, the Madlib service should be notified so the app knows the madlib story is ready.

As you can see, several of these features rely on the Madlibs service, so let's make some updates there before we tackle the Progress Bar component itself.

Add Features to the Madlibs Service

Open the

madlibs.service.ts
file:

// src/app/madlibs.service.ts
...
export class MadlibsService {
  ...
  madlibReady = false;
  pronoun: any;

  ...

  setMadlibReady(val: boolean) {
    this.madlibReady = val;
  }

  setPronoun(obj) {
    this.pronoun = obj;
  }

  ...

  getPronoun$() {
    return this.http
      .get(`${this._API}pronoun/gendered`)
      .catch(this._errorHandler);
  }

}

First we'll add some new properties:

madlibReady
to indicate when the timer observable has completed, and the
pronoun
object acquired from the API.

We'll need a way for components to set the value of

madlibReady
, so we'll create a setter method called
setMadlibReady()
that accepts a boolean argument that updates the value of the
madlibReady
property.

We'll also need a way for components to set the value of

pronoun
, so we'll create another setter method called
setPronoun()
.

In this fashion,

madlibReady
and
pronoun
are data stored in the Madlibs service, but set by other components. This way, they are accessible anywhere in the app.

Finally, we'll add a

getPronoun$()
HTTP request that returns an observable. This request fetches a pronoun object from our API, which can be accessed via subscription in our Progress Bar component.

Progress Bar Component Class

Now let's generate the new Progress Bar component with the Angular CLI:

$ ng g component progress-bar

Open the

progress-bar.component.ts
file:

// src/app/progress-bar/progress-bar.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/timer';
import 'rxjs/add/operator/takeUntil';
import { MadlibsService } from './../madlibs.service';

@Component({
  selector: 'app-progress-bar',
  templateUrl: './progress-bar.component.html',
  styleUrls: ['./progress-bar.component.scss']
})
export class ProgressBarComponent implements OnInit, OnDestroy {
  progress = 0;
  progress$: Observable<number>;
  progressSub: Subscription;
  width: string;
  submitSub: Subscription;
  pronounSub: Subscription;

  constructor(private ml: MadlibsService) { }

  ngOnInit() {
    this._setupProgress();
    this._setupSubmit();
  }

  private _setupProgress() {
    this.progress$ = Observable
      .timer(0, 50)
      .takeUntil(Observable.timer(2850));
  }

  private _getPronoun() {
    this.pronounSub = this.ml.getPronoun$()
      .subscribe(res => this.ml.setPronoun(res));
  }

  private _setupSubmit() {
    this.submitSub = this.ml.submit$
      .subscribe(words => this._startProgress());
  }

  private _startProgress() {
    this._getPronoun();
    this.progressSub = this.progress$
      .subscribe(
        p => {
          this.progress = p * 2;
          this.width = this.progress + '%';
        },
        err => console.warn('Progress error:', err),
        () => this.ml.setMadlibReady(true)
      );
  }

  ngOnDestroy() {
    if (this.progressSub) {
      this.progressSub.unsubscribe();
    }
    if (this.pronounSub) {
      this.pronounSub.unsubscribe();
    }
    this.submitSub.unsubscribe();
  }

}

Let's go over this code step by step.

We'll need to import several things. Let's import

OnDestroy
so we can clean up subscriptions when the component is destroyed. We'll also need
Subscription
and
Observable
from RxJS. We'll use methods from
MadlibsService
so we'll import that as well.

Next we'll set up some properties. The

progress
property will be used to track the number (out of 100) that represents the status of the progress bar. The
progress$
property has a type annotation indicating that we'll use it as an observable that emits numbers. We'll then subscribe to this observable with
progressSub
. We'll also create a
width
string to store a style we can use to set width with CSS in the template for the progress bar. Finally, we need
submitSub
and
pronounSub
subscriptions.

We'll make the

MadlibsService
available to the component in our constructor function.

On initialization of the component (

ngOnInit()
lifecycle function), we'll
_setupProgress()
and
_setupSubmit()
.

Create Progress Observable

Let's take a closer look at the private

_setupProgress()
method:

  private _setupProgress() {
    this.progress$ = Observable
      .timer(0, 50)
      .takeUntil(Observable.timer(2850));
  }

Here we're creating a custom observable. This observable uses the RxJS

timer
operator. It emits the first value after
0
seconds, and then emits subsequent values every
50
milliseconds.

Note: We want to emit values often to make the animation of our progress bar reasonably smooth in the browser.

The RxJS

takeUntil
operator discards any items emitted by our
timer
observable after a second observable (passed as a parameter) emits or terminates. This way, we can end our
timer
observable once a certain amount of time has elapsed. In this case, we'll run our first
timer
observable until a second one has run for
2850
ms. This way, the
progress$
observable runs for long enough to emit values from 0 to 49. We'll work with these values when we subscribe to the
progress$
observable.

Get Pronoun and Set Up Submit

Let's review the next two functions.

The private

_getPronoun()
method sets up the
pronounSub
subscription. It uses the Madlibs service method
getPronoun$()
, subscribing to the returned observable. On emission of a value, the Madlibs service method
setPronoun()
is executed, storing the pronoun in the service for access throughout the application.

The private

_setupSubmit()
method sets up the
submitSub
subscription. It subscribes to the Madlibs service's
submit$
subject. On emission of a value (e.g., the words form has been submitted), a
_startProgress()
function is run.

Start Progress Bar

The

_startProgress()
function is executed when the user submits the form with their desired words for the madlib story. The method looks like this:

  private _startProgress() {
    this._getPronoun();
    this.progressSub = this.progress$
      .subscribe(
        p => {
          this.progress = p * 2;
          this.width = this.progress + '%';
        },
        err => console.warn('Progress error:', err),
        () => this.ml.setMadlibReady(true)
      );
  }

While the progress bar is running, we want to execute

_getPronoun()
to fetch a pronoun from the API. Then we'll subscribe to our
progress$
observable with
progressSub
. This subscription makes use of the
onNext
,
onError
, and
onCompleted
methods
.

When a value is successfully emitted, we'll set our

progress
property to the value multipled by
2
. Recall that the values emitted by
progress$
range from 0 to 49. Therefore, the
progress
property will iterate in such a manner:

0, 2, 4, 6, ... 94, 96, 98

We also want to create a string value with a

%
symbol after it to style the width of the progress bar, so we'll set the
width
property appropriately.

If an error occurs, we'll log it to the console with a warning.

When the observable completes, we'll use the Madlibs service's

setMadlibReady()
setter method with an argument of
true
. This will update the service's
madlibReady
property, which is accessible throughout the app.

Unsubscribe On Destroy

Finally, we have our

ngOnDestroy()
lifecycle function. We'll check if the
progressSub
and
pronounSub
subscriptions exist. If so, we'll unsubscribe from them. They will only exist if the user submitted the words form, thus triggering the progress bar. We'll also unsubscribe from the
submitSub
subscription.

Progress Bar Component Template

For markup and styling, we'll use the Bootstrap v4 Progress Bar, so before we go much further, take a moment to familiarize yourself with its markup and customization.

Then open the

progress-bar.component.html
template and add the following markup:

<!-- src/app/progress-bar/progress-bar.component.html -->
<div class="progress">
  <div
    class="progress-bar progress-bar-striped progress-bar-animated bg-success"
    role="progressbar"
    [style.width]="width"
    [attr.aria-valuenow]="progress"
    aria-valuemin="0"
    aria-valuemax="100">
  </div>
</div>

Most of the markup is standard Bootstrap CSS. However, we have a

[style.width]
attribute which is data bound to our component's
width
property: a string that consists of a number and percentage symbol. As this member is updated by our
progressSub
subscription, the width of the progress bar UI element will change dynamically. The
attr.aria-valuenow
attribute will also be updated with the
progress
property.

Note: In Angular, the declarative data-bound attributes are not HTML attributes. Instead, they're properties of the DOM node. You can read more about this shift in the mental model in the documentation here.

Progress Bar Component Styles

Now we want to make sure our progress bar is the same height as our "Go!" button so it can fill the same space.

Open the

progress-bar.component.scss
file and add:

/* src/app/progress-bar/progress-bar.component.scss */
.progress-bar {
  font-size: 14px;
  height: 51px;
  line-height: 51px;
}

Add Progress Bar to Words Form Component

Now we have our Progress Bar component. It's time to display it in the Words Form component at the right time.

Open the

words-form.component.html
template:

<!-- src/app/words-form/words-form.component.html -->
...
  <div class="row">
    <div class="col mt-3 mb-3">
      <button
        *ngIf="!generating"
        class="btn btn-block btn-lg btn-success"
        [disabled]="!wordsForm.form.valid">Go!</button>
      <app-progress-bar [hidden]="!generating"></app-progress-bar>
    </div>
  </div>
...

First we'll add

*ngIf="!generating"
to the "Go!" button. This will remove the button after clicking, allowing us to show the progress bar in its place.

Next we'll add our

<app-progress-bar>
element below the "Go!" button near the bottom of our template. We'll use the
[hidden]
binding to hide the Progress Bar component except when
generating
is true.

Why aren't we using NgIf for the progress bar? NgIf doesn't load the component into the template at all until its expression is truthy. However, using

[hidden]
means the component initializes (but remains hidden) when the parent template loads. This will ensure the Progress Bar component is ready to go with the appropriate subscriptions already set up as soon as we might need to display it. Because it subscribes to the
submit$
subject, if we used NgIf and therefore didn't load the component until after the user clicked the "Go!" button, the progress bar wouldn't initialize properly.

Madlib Component

Our final component is the Madlib component. This component utilizes the user's words to create a silly, customized story.

Create the component with the Angular CLI like so:

$ ng g component madlib

We can now use the data we've stored in the Madlibs service to generate our story.

Madlib Component Class

Open the

madlib.component.ts
file:

// src/app/madlib/madlib.component.ts
import { Component } from '@angular/core';
import { MadlibsService } from './../madlibs.service';

@Component({
  selector: 'app-madlib',
  templateUrl: './madlib.component.html',
  styleUrls: ['./madlib.component.scss']
})
export class MadlibComponent {
  constructor(public ml: MadlibsService) { }

  aOrAn(word: string, beginSentence: boolean) {
    const startsWithVowel = ['a', 'e', 'i', 'o', 'u'].indexOf(word.charAt(0).toLowerCase()) !== -1;

    if (startsWithVowel) {
      return beginSentence ? 'An' : 'an';
    } else {
      return beginSentence ? 'A' : 'a';
    }
  }

}

This is a simple component. Most of the meat and potatoes will be in the template, which displays the actual story. We need to import the

MadlibsService
to gain access to the stored data. We'll make this available to our template publicly in the constructor function.

Then we need a simple

aOrAn()
method that returns different capitalizations of "a" or "an" depending on the word it precedes and whether or not it's at the beginning of a sentence. If the
word
argument starts with a vowel, we'll return "an". If not, we'll return "a". We'll also implement logic for sentence capitalization.

Madlib Component Template

Now it's time to display the user's completed madlib. Open the

madlib.component.html
template file and add:

<!-- src/app/madlib/madlib.component.html -->
<div class="row">
  <div class="col">
    <div class="jumbotron lead">
      <p>{{aOrAn(ml.words.adjs[0], true)}} {{ml.words.adjs[0]}} {{ml.words.nouns[0]}} {{ml.words.verbs[2]}} to the {{ml.words.nouns[1]}}. There, {{ml.pronoun.normal}} decided that it would be a good idea to test {{ml.pronoun.possessive}} mettle by doing some {{ml.words.verbs[0]}} with {{aOrAn(ml.words.nouns[3], false)}} {{ml.words.nouns[3]}}. To {{ml.pronoun.possessive}} surprise, the results made {{ml.pronoun.third}} {{ml.words.adjs[1]}}.</p>
      <p>When the initial shock wore off, {{ml.pronoun.normal}} was {{ml.words.adjs[2]}} and {{ml.pronoun.normal}} {{ml.words.verbs[2]}}. It had been {{aOrAn(ml.words.adjs[3], false)}} {{ml.words.adjs[3]}} day, so {{ml.pronoun.normal}} left the {{ml.words.nouns[1]}} and {{ml.words.verbs[3]}} to the {{ml.words.adjs[4]}} {{ml.words.nouns[2]}} {{ml.pronoun.normal}} called home.</p>
      <p>After {{ml.words.verbs[1]}} for a little while, the {{ml.words.nouns[0]}} {{ml.words.verbs[4]}} and settled down for the night with {{ml.pronoun.possessive}} {{ml.words.nouns[4]}}.</p>
    </div>
  </div>
</div>
<div class="row">
  <div class="col mt-3 mb-3">
    <button
      class="btn btn-block btn-lg btn-primary"
      (click)="ml.setMadlibReady(false)">Play Again</button>
  </div>
</div>

This template displays the story text using the data stored in our Madlibs service, including the

words
and
pronoun
objects.

At the bottom, we'll display a "Play Again" button that executes the

setMadlibReady()
setter, setting
madlibReady
to
false
. This will hide the Madlib component and show the appropriate Listen or Keyboard component again so the user can enter new words to generate another variation of the story.

Add Madlib Component to App Component

Now we need to add our Madlib component to our App component and conditionally hide it when we're showing the word entry components.

First we'll update the

app.component.ts
class:

// src/app/app.component.ts
...
import { MadlibsService } from './madlibs.service';

export class AppComponent {
  constructor(
    public speech: SpeechService,
    public ml: MadlibsService) { }
}

We need to import the

MadlibsService
and make it available via the constructor so we can use its properties in our template.

Now open the

app.component.html
template and make the following updates:

<!-- src/app/app.component.html -->
<div class="container">
  <h1 class="text-center">Madlibs</h1>
  <ng-template [ngIf]="!ml.madlibReady || !ml.pronoun">
    <app-listen *ngIf="speech.speechSupported"></app-listen>
    <app-keyboard *ngIf="!speech.speechSupported"></app-keyboard>
  </ng-template>
  <app-madlib *ngIf="ml.madlibReady && ml.pronoun"></app-madlib>
</div>

We'll wrap the Listen and Keyboard components in an

<ng-template>
with an
[ngIf]
directive and an expression that is true when the Madlibs service's properties
madlibReady
or
pronoun
are falsey. Either of these would indicate that the madlib story is not ready.

We'll then add the

<app-madlib>
element and display it if both
madlibReady
and
pronoun
are truthy. This ensures we have all the data necessary to display a complete madlib story.

Try out your madlib! The Madlib component should look something like this in the browser:

generated madlib story

Conclusion

We covered a lot while building a fun little app that generates madlibs. We were able to experience a rapidly-approaching future for web interactivity with the Web Speech API, and we learned about RxJS observables and component communication in Angular. Finally, we learned how to authenticate an Angular app and Node API with Auth0, which you can integrate into your Madlibs app if you wish as a little bit of homework.

Hopefully you now have a better understanding of Angular, speech recognition, and RxJS and are prepared to build your own more complex apps with these technologies!