Intro

Prerequisites

  • Install Node.js.
  • Install angular-cli: npm i -g @angular/cli
  • Generate project: ng new angular-youtube-search-app

Create YouTube search service

Create service component with Angular-Cli: ng g s youtube-search

We will get empty service class:

import { Injectable } from '@angular/core';

@Injectable()
export class YoutubeSearchService {

  constructor() { }

}

Declare constants

First, let’s declare constants - search URL and API key generated with Google Console API Manager:

export const YOUTUBE_API_KEY = USE_YOUR_OWN_API_KEY_HERE;
export const YOUTUBE_API_URL = 'https://www.googleapis.com/youtube/v3/search';

Create constructor for the service

Then we will create constructor injecting Http service and constants, the body of the constructor will be empty as we do not do anything special, except for providing parameters with dependency injection mechanism and they will be automatically converted to private fields:

constructor(private http: Http,
    @Inject(YOUTUBE_API_KEY) private apiKey: string,
    @Inject(YOUTUBE_API_URL) private apiUrl: string) {
}

We also need to modify import declarations of the service:

import { Injectable, Inject } from '@angular/core';
import { Http, Response } from '@angular/http';

Add HttpModule to app.module.ts

We also need to declare imported module in app.module.ts:

import { HttpModule } from '@angular/http';

//...
  imports: [
    BrowserModule,
    HttpModule
  ],
//...

Declare YoutubeVideo

We will create regular TypeScript class YoutubeVideo that will hold search result data:

export class YoutubeVideo {
  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}`;
  }
}

And add import declaration to the service:

import { YoutubeVideo } from './youtube-video.model';

Create search() method

Then we will implement search method that will return Observable array of YoutubeVideo items:

We need to import Observable first:

import { Observable } from 'rxjs/Observable';

Search method (youtube-search.service.ts):

  search(query: string): Observable <YoutubeVideo[] > {
    const params = `q=${query}&key=${this.apiKey}&part=snippet&type=video&maxResults=10`;
    const queryUrl = `${this.apiUrl}?${params}`;
    return this.http.get(queryUrl)
      .map((response: Response) => {
        return (<any>response.json()).items.map(item => {
          return new YoutubeVideo({
            id: item.id.videoId,
            title: item.snippet.title,
            description: item.snippet.description,
            thumbnailUrl: item.snippet.thumbnails.high.url
          });
        });
      });
  }

Create search form

Let’s generate new component search-form with inline template and styles: ng g c search-form -it -is

Set template to:

<input type="text" class="form-control" placeholder="Search" autofocus>

Add imports:

import {
  Component,
  OnInit,
  Output,
  EventEmitter,
  ElementRef
} from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/switch';
import { YoutubeSearchService } from './../youtube-search.service';
import { YoutubeVideo } from './../youtube-video.model';

Implement logic of the class:

export class SearchFormComponent implements OnInit {
  @Output() loading: EventEmitter<boolean> = new EventEmitter<boolean>();
  @Output() results: EventEmitter<YoutubeVideo[]> = new EventEmitter<YoutubeVideo[]>();

  constructor(private youtube: YoutubeSearchService,
              private el: ElementRef) {
  }

  ngOnInit(): void {
    // convert the `keyup` event into an observable stream
    Observable.fromEvent(this.el.nativeElement, 'keyup')
      .map((e: any) => e.target.value) // extract the value of the input
      .filter((text: string) => text.length > 1) // filter out if empty
      .debounceTime(250)                         // only once every 250ms
      .do(() => this.loading.emit(true))         // enable loading
      // search, discarding old events if new input comes in
      .map((query: string) => this.youtube.search(query))
      .switch()
      // act on the return of the search
      .subscribe(
        (results: YoutubeVideo[]) => { // on sucesss
          this.loading.emit(false);
          this.results.emit(results);
        },
        (err: any) => { // on error
          console.log(err);
          this.loading.emit(false);
        },
        () => { // on completion
          this.loading.emit(false);
        }
      );
  }
}

Create search-result component

ng g c search-result

<div class="col-sm-6 col-md-3">
   <div class="thumbnail">
     <img src="{{result.thumbnailUrl}}">
     <div class="caption">
       <h3>{{result.title}}</h3>
       <p>{{result.description}}</p>
       <p><a href="{{result.videoUrl}}"
             class="btn btn-default" role="button">
             Watch</a></p>
     </div>
   </div>
 </div>
import { Component, OnInit, Input } from '@angular/core';

import { YoutubeVideo } from './../youtube-video.model';

@Component({
  selector: 'app-search-result',
  templateUrl: './search-result.component.html',
  styleUrls: ['./search-result.component.css']
})
export class SearchResultComponent implements OnInit {
  @Input() result: YoutubeVideo;

  constructor() { }

  ngOnInit() {
  }

}

Create search component

Add image to assets/loading.gif:

ng g c search:

<div class='container'>
    <div class="page-header">
      <h1>YouTube Search
        <img
          style="float: right;"
          *ngIf="loading"
          src='assets/images/loading.gif' />
      </h1>
    </div>

    <div class="row">
      <div class="input-group input-group-lg col-md-12">
        <app-search-form
           (loading)="loading = $event"
           (results)="updateResults($event)">
        </app-search-form>
      </div>
    </div>

    <div class="row">
      <app-search-result
        *ngFor="let result of results"
        [result]="result">
      </app-search-result>
    </div>
</div>

Add providers to app.module.ts:

import { YoutubeSearchService, YOUTUBE_API_KEY, YOUTUBE_API_URL } from './youtube-search.service';

  //...
  providers: [
    YoutubeSearchService,
    {provide: YOUTUBE_API_KEY, useValue: YOUTUBE_API_KEY},
    {provide: YOUTUBE_API_URL, useValue: YOUTUBE_API_URL}
  ],
  //...

Modify app-component.html:

<app-search-component></app-search-component>

Add bootstrap to the project

npm i bootstrap

angular-cli.json:

{
//...
      "styles": [
        "styles.css",
        "../node_modules/bootstrap/dist/css/bootstrap.min.css"
      ],
//...
}