Angular 6 Search Box example with Youtube API & RxJS 6

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

angular-search-box-example-youtube-api

Technology

– Angular 6
– RxJS 6
– YouTube v3 search API

Project Structure

angular-search-box-example-youtube-api-angular-tutorial-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>grokonez 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:';
    }
  }
}

Source Code

angular-youtube-search



By grokonez | November 28, 2018.

Last updated on April 17, 2021.



Related Posts


1 thought on “Angular 6 Search Box example with Youtube API & RxJS 6”

  1. Nice article.
    But, I have some issue.
    I already create Google Project & Add API key but error still show.

    HttpErrorResponse {headers: HttpHeaders, status: 403, statusText: “OK”, url: “https://www.googleapis.com/youtube/v3/search?q=art…zN4ZiBSr-b4&part=snippet&type=video&maxResults=10”, ok: false, …}

    Any suggestion?

    Thanks

Got Something To Say:

Your email address will not be published. Required fields are marked *

*