To interact with the YouTube API, we need to get an API Key from the Google Console, so
. You will have to create an app to obtain API credentials for it. This will enable our app to submit API requests to the YouTube API.
const API_KEY = 'your-api-key-here'; export default API_KEY;
After this, you will import the API key into the `app.component.ts` file which is where we will write the code to query the YouTube API. Do it like this:
import API_KEY from './api-key';
Query YouTube API
The YouTube API doesn’t return a direct list of results, but rather a group of meta data where it has one property called Items that contains the list of results.
Next, let’s build our form in the
src/app/app.component.ts file. Add the following code:
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import API_KEY from './api-key';
const API_URL = 'https://www.googleapis.com/youtube/v3/search';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Angular RxJs YouTube Searcher';
searchForm: FormGroup;
results: any;
constructor(private formBuilder: FormBuilder, private http: HttpClient) {
this.searchForm = this.formBuilder.group({
search: ['', Validators.required]
});
this.searchForm.controls.search.valueChanges.subscribe(result => console.log(result));
}
}
The Search Input Form is using the Angular Reactive Forms API. Learn more about reactive forms here.
Because the `valueChanges` method returns an Observable, here in our example it returns an Observable of characters typed in the input element. The subscribe function call subscribes to each value, saves them in the result variable and displays that in the browser console with console.log.
We are also using the arrow function expression=> ) on the subscribe call, which has a shorter syntax than the function expression. It’s very efficient when you want to write short functions like the type you would use in a subscribe method. Read more about arrow function here.
Using the Angular Reactive Form API
To start using the Reactive Forms API and HTTP Client, we need to import the modules into the `app.module.ts` file, so update the module with the following imports and add the modules to the imports array:
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
.....
imports: [
BrowserModule,
ReactiveFormsModule,
HttpClientModule
],
......
And in the template file, src/app/app.component.html, add the following code:
<h3>{{ title }}</h3>
<form [formGroup]="searchForm">
<label for="search">Search YouTube</label>
<br />
<input formControlName="search" id="search" />
</form>
Also note that the `searchForm` and search both correspond to the names given in the template, because that’s how Angular knows how to connect the template html code to the typescript component code.
Running the Angular App
Let’s run the Angular app through the server view in Angular IDE. In order to open it, select `Window`, then `Show view`, then `Servers`. Right click on `youtube-searcher` and click `Start Server`. Angular IDE serves the application on localhost port 4200 by default, so open up in your browser to see the running app.
We won’t be doing anything with our search for now, we just need to verify that the form is working. To open up the Console, right click anywhere on the page of your browser, click Inspect, and select the Console tab.
As you can see, everything that is typed into the Input form gets logged out – that’s the first example showing how Angular leverages the power of the Observable method.
Next, we need to pass that value now and use it to query the YouTube API for results. Let’s do that next. Update the search section in file to look like this (we will console.log the value that we get from the YouTube API).
this.searchForm.controls.search.valueChanges.subscribe(searchTerm => {
this.http.get<any>(
`${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`)
.subscribe(result => {
console.log(result);
});
});
The result should look like this when you now try to search. As you can see, we are getting back the responses from the API, and if you explore well, you will see the Items object in the response.
Improve and Refactor the Code
However, we now have some issues. First, you will notice that currently we query the API every time we type in a word into the input form. This is not what we would want, else we would quickly exceed our usage and query limit. Therefore, we will use some operators from the RxJS library to clean up the code and achieve the desired result.
First, let’s move the whole search function into a separate method. Let’s call it `search`. Then we will call it inside ngOnInit instead of in the constructor, as we are currently doing. In a larger Angular app, it is a bad practice to call functions, especially Angular functions inside the constructor. You don’t want to call any of your Angular functions inside the constructor because Angular does not have any control over when the constructor gets called or initiated on page load.
You can read more here on the difference between ngOnInit and the constructor.
After refactoring the code, the result looks like this. Notice the importation of `OnInit` from`@angular/core` on line 1 and its implementation on line 20. After this update, your app’s behavior should remain the same. Go ahead and give it a try.
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import API_KEY from './api-key';
const API_URL = 'https://www.googleapis.com/youtube/v3/search';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'Angular RxJs YouTube Searcher';
searchForm: FormGroup;
results: any;
constructor(private formBuilder: FormBuilder, private http: HttpClient) {
this.searchForm = this.formBuilder.group({
search: ['', Validators.required]
});
}
ngOnInit() {
this.search();
}
search() {
this.searchForm.controls.search.valueChanges.subscribe(searchTerm => {
this.http.get<any>(
`${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`)
.subscribe(result => {
console.log(result);
});
});
}
}
So back to getting the desired result, our first issue is with the nested subscription, which is common when using Promises. This sort of nesting is called the pyramid of doom, which is one of the major reasons we don’t want to use *a* Promise in the first place. *We* want to avoid that at all *costs*, and to do that, Observable provides an operator called Pipeable Operator, which allows you to *do this* nesting of operations in a more efficient way.
Thus, we will refactor our code to leverage the pipe. The first operator we will use is the `switchMap` operator.
Understanding Operators in RxJS
`switchMap` will cancel out the prior event and just use the value in the latest event. With `switchMap`, every time a new Observable is returned, it unsubscribes from the previous one and subscribes to the latest one. Basically, its switching from one Observable to the latest Observable, while `mergeMap` will let all the Observables go (it won’t cancel the old ones – this is where you could get the race conditions, which results in a weird behavior where the result of the first event could be returned instead of the desired one).
Read more about race conditions and about the different types of
Observable maps.
Now, with the application of switchMap, should you test this out, you will notice that the request is not sent to the API upon every keystroke anymore.
import { switchMap } from 'rxjs/operators';
search() {
this.searchForm.controls.search.valueChanges.pipe(
switchMap(searchTerm => this.http.get<any>(
`${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`))
).subscribe(result => console.log(result));
}
But something still seems off. If you are on a fast network, you will notice that at least two objects are still being returned, which means the subscription didn’t wait for you to finish typing your query before the request was sent. We can delay the sending of request a little bit by adding another operator called debounceTime, a really helpful operator that will debounce the events. It will discard emitted values that takes less than the specified time between outputs.
import { switchMap, debounceTime } from 'rxjs/operators';
search() {
this.searchForm.controls.search.valueChanges.pipe(
debounceTime(500),
switchMap(searchTerm => this.http.get<any>(
`${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`))
).subscribe(result => console.log(result));
}
So `switchMap` cancelled prior request but only at the browser level. Once those network requests go out, they are still hitting the server, which means an unwanted request might still be sent. Now debounce will make the request wait 500 milliseconds until the events are fired(until requests are sent to the server) – events will not be sent to the server until you have finished typing in the request. This means only one API call/request will go to the server.
We can also add other operators like
filter and
distinctUntilChanged. Filter only emits those items from an Observable that pass a predicate test, while `distinctUntilChanged` only emits when the current value is different from the last.
We also have to apply a map operator to map through the values that are returned from the API, because the request returns a big object with different metadata, and the only object we need from this is the Items object.
The final code looks like this:
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { map, switchMap, debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
import API_KEY from './api-key';
const API_URL = 'https://www.googleapis.com/youtube/v3/search';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'Angular RxJs YouTube Searcher';
searchForm: FormGroup;
results: Observable<any>;
constructor(private formBuilder: FormBuilder, private http: HttpClient) {
this.searchForm = this.formBuilder.group({
search: ['', Validators.required]
});
}
ngOnInit() {
this.search();
}
search() {
this.results = this.searchForm.controls.search.valueChanges.pipe(
debounceTime(500),
filter(value => value.length > 3),
distinctUntilChanged(),
switchMap(searchTerm => this.http.get<any>(`${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`)),
map(response => response.items)
);
}
}
Angular’s HTTP method returns an Observable instead of returning a Promise. This Observable then needs to be subscribed to for it to be consumed. Using the pipeable operator on the subscription, we are able to transform the returned subscription, and now we need to use async pipe to consume the subscription on the template side.
To make Angular and RxJS better together, Angular has created an async pipe, used in the template.
While it’s possible to do a standard subscription on the component side, it can also be done in the declarative template style, which is generally recommended by Angular. Async will automatically subscribe to the Observable for you, and it will automatically unsubscribe for you as well when you navigate out of the page or component.
And in the HTML template we have this:
<h3>{{ title }}</h3>
<form [formGroup]="searchForm">
<label for="search">Search YouTube</label>
<br />
<input formControlName="search" id="search" />
<div *ngFor="let result of results | async">
<a [href]="'https://www.youtube.com/watch?v=' + result.id.videoId" target="_blank">
{{result.snippet.title}}
</a>
<p>{{result.snippet.description}}</p>
<img [src]="result.snippet.thumbnails.default.url" style="width: 100%; max-width: 250px;" />
</div>
</form>
What the async pipe does depends on whether you give it a Promise or an Observable. It creates and unwraps the subscription or Promise, and displays the data when the component is loaded, when the template is running, and then automatically unloads and unsubscribes when the component is unloadedfor example when you navigate to another page with a new component). This manages the whole life cycle of subscription to Observables, so you don’t have to be managing any of that yourself.
Your result should now look like this:Points to take note of:
- With a Promise you can only handle one event
- With an Observable you can handle multiple events
- .subscribe() is similar to .then()
- An Observable can do everything that a Promise can do, plus more
- Use Angular’s HttpClient to handle API calls.
- The Angular framework uses a lot of RxJS
- Many Angular APIs use Observables – we can take advantage of this to build some really cool stuff
Conclusion
RxJS is a really important part of your Angular toolbox – even though it doesn’t solve every problem, it will definitely make you a better Angular developer. Asynrhonous experiences are the norm for today’s websites. With the combined power of Angular and RxJS, you’re well on your way to delivering this experience to your users too.