In this tutorial, we’re gonna build an Angular Application that helps us to search YouTube when typing. The result is a list of video thumbnails, along with a description and link to each YouTube video. We’ll use RxJS 6 for processing data and EventEmitter
for interaction between Components.
Angular 6 Search Box example with Youtube API overview
Goal
Technology
– Angular 6
– RxJS 6
– YouTube v3 search API
Project Structure
– VideoDetail
object (video-detail.model) holds the data we want from each result.
– YouTubeSearchService
(youtube-search.service) manages the API request to YouTube and convert the results into a stream of VideoDetail[]
.
– SearchBoxComponent
(search-box.component) calls YouTube service when the user types.
– SearchResultComponent
(search-result.component) renders a specific VideoDetail
.
– AppComponent
(app.component) encapsulates our whole YouTube searching app and
render the list of results.
Practice
Setup Project
Create Service & Components
Run commands:
– ng g s services/youtube-search
– ng g c youtube/search-box
– ng g c youtube/search-result
Add HttpClient module
Open app.module.ts, add HttpClientModule
:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { AppComponent } from './app.component'; import { SearchBoxComponent } from './youtube/search-box/search-box.component'; import { SearchResultComponent } from './youtube/search-result/search-result.component'; @NgModule({ declarations: [ AppComponent, SearchBoxComponent, SearchResultComponent ], imports: [ BrowserModule, HttpClientModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Result DataModel
youtube/video-details.model.ts
export class VideoDetail { id: string; title: string; description: string; thumbnailUrl: string; videoUrl: string; constructor(obj?: any) { this.id = obj && obj.id || null; this.title = obj && obj.title || null; this.description = obj && obj.description || null; this.thumbnailUrl = obj && obj.thumbnailUrl || null; this.videoUrl = obj && obj.videoUrl || `https://www.youtube.com/watch?v=${this.id}`; } }
Youtube Search Service
We use YouTube v3 search API.
In order to use this API, we need to have an API key. To generate the key, open Credentials page, create new Project, Create credentials
=> API key.
Once you get the API key, replace the string 'xxx'
for YOUTUBE_API_KEY
in the code below:
services/youtube-search.service.ts
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { map } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { VideoDetail } from '../youtube/video-detail.model'; const YOUTUBE_API_KEY = 'xxx'; const YOUTUBE_API_URL = 'https://www.googleapis.com/youtube/v3/search'; @Injectable({ providedIn: 'root' }) export class YoutubeSearchService { constructor(private http: HttpClient) { } search(query: string): Observable{ const params: string = [ `q=${query}`, `key=${YOUTUBE_API_KEY}`, `part=snippet`, `type=video`, `maxResults=10` ].join('&'); const queryUrl = `${YOUTUBE_API_URL}?${params}`; return this.http.get(queryUrl).pipe(map(response => { return response['items'].map(item => { return new VideoDetail({ id: item.id.videoId, title: item.snippet.title, description: item.snippet.description, thumbnailUrl: item.snippet.thumbnails.high.url }); }); })); } }
search()
function takes a query
string and returns an Observable
that will emit a stream of VideoDetail[]
:
– from query
string, we build the queryUrl
by concatenating the YOUTUBE_API_URL
and the params
.
– then we use HttpClient.get()
method, take the return value and use map()
to get the response, iterate over each item and convert it to a VideoDetail
.
Search Box Component
This component will :
– Watch for keyup
on an input and call search() function of YouTubeSearchService
.
– Emit a loading
event when we’re loading (or not).
– Emit a results
event when we have new results.
youtube/search-box.component.html
<input type="text" class="form-control" placeholder="Search" autofocus>
youtube/search-box.component.ts
import { Component, OnInit, Output, EventEmitter, ElementRef } from '@angular/core'; import { fromEvent } from 'rxjs'; import { map, filter, debounceTime, tap, switchAll } from 'rxjs/operators'; import { VideoDetail } from '../video-detail.model'; import { YoutubeSearchService } from 'src/app/services/youtube-search.service'; @Component({ selector: 'app-search-box', templateUrl: './search-box.component.html', styleUrls: ['./search-box.component.css'] }) export class SearchBoxComponent implements OnInit { @Output() loading = new EventEmitter(); @Output() results = new EventEmitter (); constructor(private youtube: YoutubeSearchService, private el: ElementRef) { } ngOnInit() { // convert the `keyup` event into an observable stream fromEvent(this.el.nativeElement, 'keyup').pipe( map((e: any) => e.target.value), // extract the value of the input filter(text => text.length > 1), // filter out if empty debounceTime(500), // only once every 500ms tap(() => this.loading.emit(true)), // enable loading map((query: string) => this.youtube.search(query)), // search switchAll()) // produces values only from the most recent inner sequence ignoring previous streams .subscribe( // act on the return of the search _results => { this.loading.emit(false); this.results.emit(_results); }, err => { console.log(err); this.loading.emit(false); }, () => { this.loading.emit(false); } ); } }
Two @Outputs
specify that events will be emitted from this component. So we can use the (output)="callback()"
syntax in parent component to listen to events on this component.
In this example, we will use the app-search-box
tag later (App Component):
<app-search-box (loading)="loading = $event" (results)="updateResults($event)"></app-search-box>
In constructor()
method we inject :
– YouTubeSearchService
– el
element: an object of type ElementRef
, which is an Angular wrapper around a native element.
On this input box we want to watch for keyup
events, get input value, and:
– filter out any empty or short queries: filter()
– debounce the input, that is, don’t search on every character but only after the user has
stopped typing after a short amount of time: debounceTime()
– discard any old searches, if the user has made a new search: switchAll()
subscribe
with three arguments:
– onSuccess:
+ this.loading.emit(false)
indicates stop loading
+ this.results.emit(results)
emits an event containing the list of results
– onError: when the stream has an error event:
+ log out the error
+ set this.loading.emit(false)
– onCompletion: when the stream completes, set this.loading.emit(false)
to indicate that we’re done loading.
Search Results Component
youtube/search-result.component.ts
import { Component, OnInit, Input } from '@angular/core'; import { VideoDetail } from '../video-detail.model'; @Component({ selector: 'app-search-result', templateUrl: './search-result.component.html', styleUrls: ['./search-result.component.css'] }) export class SearchResultComponent implements OnInit { @Input() result: VideoDetail; constructor() { } ngOnInit() { } }
youtube/search-result.component.html
<div class="col-sm-6 col-md-4"> <div class="thumbnail"> <img src="{{result.thumbnailUrl}}"> <div class="caption"> <h4>{{result.title}}</h4> <p>{{result.description}}</p> <p><a href="{{result.videoUrl}}" class="btn btn-default" role="button"> Watch</a> </p> </div> </div> </div>
App Component
In this parent component, we will:
– show the loading indicator when loading
– listen to events from the search-box.component
– show the search results
app.component.html
<div class='container'> <div class="page-header"> <h2>ozenero YouTube Search <img style="float: right;" *ngIf="loading" src='assets/images/loading.gif' /> </h2> </div> <div class="row"> <div class="input-group input-group-lg col-sm-8 col-md-8"> <app-search-box (loading)="loading = $event" (results)="updateResults($event)"></app-search-box> </div> </div> <div class="row" style="margin-top:20px"> <p>{{message}}</p> <app-search-result *ngFor="let result of results" [result]="result"></app-search-result> </div> </div>
app.component.ts
import { Component } from '@angular/core'; import { VideoDetail } from './youtube/video-detail.model'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { results: VideoDetail[]; loading: boolean; message = ''; updateResults(results: VideoDetail[]): void { this.results = results; if (this.results.length === 0) { this.message = 'Not found...'; } else { this.message = 'Top 10 results:'; } } }