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>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:';
    }
  }
}

Source Code

angular-youtube-search

25 thoughts 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

  2. One thing is one of the most popular incentives for making use of your card is a cash-back as well as rebate supply. Generally, you’ll have access to 1-5 back on various expenses. Depending on the card, you may get 1 again on most purchases, and 5 back on expenditures made using convenience stores, filling stations, grocery stores plus ‘member merchants’.

  3. Hello just wanted to give you a quick heads up.
    The words in your article seem to be running off the screen in Ie.
    I’m not sure if this is a formatting issue or something to
    do with web browser compatibility but I figured I’d post to let you
    know. The design look great though! Hope you get the problem solved soon. Many thanks

  4. 474482 347793I like this post a whole lot. I will undoubtedly be back. Hope that I is going to be able to read far more insightful posts then. Will probably be sharing your information with all of my associates! 861529

  5. Each of us includes a distinct prerequisite from your slot game that
    people may perform in, but knowing where you can search for them
    is just as essential.

  6. There are some attention-grabbing closing dates in this article however I don’t know if I see all of them heart to heart. There is some validity but I’ll take maintain opinion until I look into it further. Good article , thanks and we would like extra! Added to FeedBurner as nicely

  7. Great submit. I simply discovered your blog and wanted to claim that We have actually appreciated browsing your blog posts. Regardless I will be subscribing to your nourish and that i we do hope you create again shortly!

  8. The clean Zune browser is amazingly Terrific, but not as superior as the iPod’s. It works perfectly, nevertheless is not as immediate as Safari, and incorporates a clunkier interface. If your self often system on making use of the world-wide-web browser that is not an issue, but if you are building to read the net alot against your PMP then the iPod’s much larger screen and better browser could be critical.

  9. 631095 55400Nowhere on the Internet is there this a lot quality and clear information on this topic. How do I know? I know because Ive searched this topic at length. Thank you. 124375

  10. I have to point out my appreciation for your kindness for men and women who require help with that area of interest. Your real dedication to getting the solution all through turned out to be particularly significant and have made some individuals just like me to attain their aims. Your entire important instruction entails a lot a person like me and substantially more to my office workers. Many thanks; from all of us.

  11. 951003 631837You produced some decent points there. I looked on the net towards the concern and discovered a lot of people go together with together together with your web internet site. 208135

  12. 883741 387386Outstanding post. I was checking constantly this blog and Im impressed! Extremely valuable details specially the last part I care for such information significantly. I was seeking this particular details for a long time. Thank you and finest of luck. 932294

Leave a Reply

Your email address will not be published.