• No results found

YOUTUBE_API_KEY 3 YOUTUBE_API_URL

In document Ng Book2 r39 (Page 182-200)

Can I get my cat to write Angular 2? For this example we’re going to write several things:

2. YOUTUBE_API_KEY 3 YOUTUBE_API_URL

Notice that we make instance variables from all three arguments, meaning we can access them as this.http,this.apiKey, andthis.apiUrlrespectively.

Notice that we explicitly inject using the@Inject(YOUTUBE_API_KEY)notation. YouTubeServicesearch

Next let’s implement thesearchfunction.searchtakes a querystringand returns anObservable which will emit a stream of SearchResult[]. That is, each item emitted is an array of SearchRe- sults.

code/http/app/ts/components/YouTubeSearchComponent.ts

58 search(query: string): Observable<SearchResult[]> { 59 let params: string = [

60 `q=${query}`,

61 `key=${this.apiKey}`, 62 `part=snippet`, 63 `type=video`, 64 `maxResults=10`

65 ].join('&');

66 let queryUrl: string = `${this.apiUrl}?${params}`;

We’re building thequeryUrlin a manual way here. We start by simply putting the query params in the paramsvariable. (You can find the meaning of each of those values byreading the search API docs⁴².)

Then we build thequeryUrlby concatenating theapiUrland theparams. Now that we have aqueryUrlwe can make our request:

code/http/app/ts/components/YouTubeSearchComponent.ts

58 search(query: string): Observable<SearchResult[]> { 59 let params: string = [

60 `q=${query}`,

61 `key=${this.apiKey}`, 62 `part=snippet`, 63 `type=video`, 64 `maxResults=10`

65 ].join('&');

66 let queryUrl: string = `${this.apiUrl}?${params}`; 67 return this.http.get(queryUrl)

68 .map((response: Response) => {

69 return (<any>response.json()).items.map(item => {

70 // console.log("raw item", item); // uncomment if you want to debug

71 return new SearchResult({ 72 id: item.id.videoId, 73 title: item.snippet.title, 74 description: item.snippet.description, 75 thumbnailUrl: item.snippet.thumbnails.high.url 76 }); 77 }); 78 }); 79 }

Here we take the return value of http.getand usemapto get theResponsefrom the request. From thatresponsewe extract the body as an object using.json()and then we iterate over each item and convert it to aSearchResult.

If you’d like to see what the rawitem looks like, just uncomment theconsole.logand

inspect it in your browsers developer console.

Notice that we’re calling(<any>response.json()).items. What’s going on here? We’re

telling TypeScript that we’re not interested in doing strict type checking.

When working with a JSON API, we don’t generally have typing definitions for the API responses, and so TypeScript won’t know that theObjectreturned even has anitemskey,

so the compiler will complain.

We could callresponse.json()["items"]and then cast that to anArrayetc., but in this

case (and in creating theSearchResult, it’s just cleaner to use ananytype, at the expense

YouTubeServiceFull Listing

Here’s the full listing of ourYouTubeService:

code/http/app/ts/components/YouTubeSearchComponent.ts

1 /**

2 * YouTubeSearchComponent is a tiny app that will autocomplete search YouTube.

3 */ 4 5 import { 6 Component, 7 Injectable, 8 OnInit, 9 ElementRef, 10 EventEmitter, 11 Inject 12 } from '@angular/core';

13 import { Http, Response } from '@angular/http'; 14 import { Observable } from 'rxjs';

15 16 /*

17 This API key may or may not work for you. Your best bet is to issue your own

18 API key by following these instructions:

19 https://developers.google.com/youtube/registering_an_application#Create_API_Ke\

20 ys

21

22 Here I've used a **server key** and make sure you enable YouTube.

23

24 Note that if you do use this API key, it will only work if the URL in

25 your browser is "localhost"

26 */

27 export var YOUTUBE_API_KEY: string = 'AIzaSyDOfT_BO81aEZScosfTYMruJobmpjqNeEk'; 28 export var YOUTUBE_API_URL: string = 'https://www.googleapis.com/youtube/v3/sear\

29 ch';

30 let loadingGif: string = ((<any>window).__karma__) ? '' : require('images/loadin\

31 g.gif'); 32 33 class SearchResult { 34 id: string; 35 title: string; 36 description: string; 37 thumbnailUrl: string; 38 videoUrl: string;

39

40 constructor(obj?: any) {

41 this.id = obj && obj.id || null;

42 this.title = obj && obj.title || null; 43 this.description = obj && obj.description || null; 44 this.thumbnailUrl = obj && obj.thumbnailUrl || null; 45 this.videoUrl = obj && obj.videoUrl ||

46 `https://www.youtube.com/watch?v=${this.id}`;

47 }

48 } 49 50 /**

51 * YouTubeService connects to the YouTube API

52 * See: * https://developers.google.com/youtube/v3/docs/search/list

53 */

54 @Injectable()

55 export class YouTubeService { 56 constructor(public http: Http,

57 @Inject(YOUTUBE_API_KEY) private apiKey: string, 58 @Inject(YOUTUBE_API_URL) private apiUrl: string) {

59 }

60

61 search(query: string): Observable<SearchResult[]> { 62 let params: string = [

63 `q=${query}`,

64 `key=${this.apiKey}`, 65 `part=snippet`, 66 `type=video`, 67 `maxResults=10`

68 ].join('&');

69 let queryUrl: string = `${this.apiUrl}?${params}`; 70 return this.http.get(queryUrl)

71 .map((response: Response) => {

72 return (<any>response.json()).items.map(item => {

73 // console.log("raw item", item); // uncomment if you want to debug

74 return new SearchResult({ 75 id: item.id.videoId, 76 title: item.snippet.title, 77 description: item.snippet.description, 78 thumbnailUrl: item.snippet.thumbnails.high.url 79 }); 80 });

81 });

82 }

83 } 84

85 export var youTubeServiceInjectables: Array<any> = [ 86 {provide: YouTubeService, useClass: YouTubeService}, 87 {provide: YOUTUBE_API_KEY, useValue: YOUTUBE_API_KEY}, 88 {provide: YOUTUBE_API_URL, useValue: YOUTUBE_API_URL} 89 ];

90 91 /**

92 * SearchBox displays the search box and emits events based on the results

93 */

94

95 @Component({

96 outputs: ['loading', 'results'], 97 selector: 'search-box',

98 template: `

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

100 `

101 })

102 export class SearchBox implements OnInit {

103 loading: EventEmitter<boolean> = new EventEmitter<boolean>();

104 results: EventEmitter<SearchResult[]> = new EventEmitter<SearchResult[]>(); 105

106 constructor(public youtube: YouTubeService,

107 private el: ElementRef) {

108 }

109

110 ngOnInit(): void {

111 // convert the `keyup` event into an observable stream

112 Observable.fromEvent(this.el.nativeElement, 'keyup')

113 .map((e: any) => e.target.value) // extract the value of the input

114 .filter((text: string) => text.length > 1) // filter out if empty

115 .debounceTime(250) // only once every 250ms

116 .do(() => this.loading.next(true)) // enable loading

117 // search, discarding old events if new input comes in

118 .map((query: string) => this.youtube.search(query)) 119 .switch()

120 // act on the return of the search

121 .subscribe(

123 this.loading.next(false); 124 this.results.next(results);

125 },

126 (err: any) => { // on error

127 console.log(err);

128 this.loading.next(false);

129 },

130 () => { // on completion

131 this.loading.next(false);

132 } 133 ); 134 135 } 136 } 137 138 @Component({ 139 inputs: ['result'], 140 selector: 'search-result', 141 template: `

142 <div class="col-sm-6 col-md-3">

143 <div class="thumbnail"> 144 <img src="{{result.thumbnailUrl}}"> 145 <div class="caption"> 146 <h3>{{result.title}}</h3> 147 <p>{{result.description}}</p> 148 <p><a href="{{result.videoUrl}}"

149 class="btn btn-default" role="button">Watch</a></p>

150 </div>

151 </div>

152 </div>

153 `

154 })

155 export class SearchResultComponent { 156 result: SearchResult; 157 } 158 159 @Component({ 160 selector: 'youtube-search', 161 template: ` 162 <div class='container'> 163 <div class="page-header"> 164 <h1>YouTube Search

165 <img 166 style="float: right;" 167 *ngIf="loading" 168 src='${loadingGif}' /> 169 </h1> 170 </div> 171 172 <div class="row">

173 <div class="input-group input-group-lg col-md-12">

174 <search-box 175 (loading)="loading = $event" 176 (results)="updateResults($event)" 177 ></search-box> 178 </div> 179 </div> 180 181 <div class="row"> 182 <search-result

183 *ngFor="let result of results"

184 [result]="result"> 185 </search-result> 186 </div> 187 </div> 188 ` 189 })

190 export class YouTubeSearchComponent { 191 results: SearchResult[];

192

193 updateResults(results: SearchResult[]): void { 194 this.results = results;

195 // console.log("results:", this.results); // uncomment to take a look

196 }

197 }

Writing the

SearchBox

TheSearchBox component plays a key role in our app: it is the mediator between our UI and the YouTubeService.

TheSearchBoxwill:

2. Emit aloadingevent when we’re loading (or not) 3. Emit aresultsevent when we have new results SearchBox @ComponentDefinition

Let’s define ourSearchBox @Component:

code/http/app/ts/components/YouTubeSearchComponent.ts

88 /**

89 * SearchBox displays the search box and emits events based on the results

90 */

91

92 @Component({

93 outputs: ['loading', 'results'], 94 selector: 'search-box',

Theselectorwe’ve seen many times before: this allows us to create a<search-box>tag.

Theoutputskey specifies events that will be emitted from this component. That is, we can use the (ouput)="callback()"syntax in our view to listen to events on this component. For example, here’s how we will use thesearch-boxtag in our view later on:

1 <search-box

2 (results)="updateResults($event)"

3 (loading)="loading = $event"

4 ></search-box>

In this example, when the SearchBox component emits aloading event, we will set the variable loading in the parent context. Likewise, when theSearchBoxemits aresults event, we will call theupdateResults()function, with the value, in the parent’s context.

In the@Component configuration we’re simply specifying the names of the events with the strings "loading"and"results". In this example, each event will have a correspondingEventEmitteras an instance variable of the controller class. We’ll implement that in a few minutes.

For now, remember that @Component is like the public API for our component, so here we’re just specifying the name of the events, and we’ll worry about implementing theEventEmitters later. SearchBox templateDefinition

code/http/app/ts/components/YouTubeSearchComponent.ts

88 /**

89 * SearchBox displays the search box and emits events based on the results

90 */

91

92 @Component({

93 outputs: ['loading', 'results'], 94 selector: 'search-box',

95 template: `

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

97 `

98 })

SearchBoxController Definition OurSearchBoxcontroller is a new class:

code/http/app/ts/components/YouTubeSearchComponent.ts

99 export class SearchBox implements OnInit {

100 loading: EventEmitter<boolean> = new EventEmitter<boolean>();

101 results: EventEmitter<SearchResult[]> = new EventEmitter<SearchResult[]>();

We say that this classimplements OnInitbecause we want to use thengOnInitlifecycle callback. If a classimplements OnInitthen thengOnInitfunction will be called after the first change detection check.

ngOnInitis a good place to do initialization (vs. theconstructor) because inputs set on a component are not available in theconstructor.

SearchBoxController Definitionconstructor

Let’s talk about theSearchBox constructor:

code/http/app/ts/components/YouTubeSearchComponent.ts

103 constructor(public youtube: YouTubeService,

104 private el: ElementRef) {

105 }

1. OurYouTubeServiceand

2. The elementelthat this component is attached to.elis an object of typeElementRef, which is an Angular wrapper around a native element.

We set both injections as instance variables.

SearchBoxController DefinitionngOnInit

On this input box we want to watch forkeyupevents. The thing is, if we simply did a search after every keyupthat wouldn’t work very well. There are three things we can do to improve the user experience:

1. Filter out any empty or short queries

2. “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

3. discard any old searches, if the user has made a new search

We could manually bind to keyup and call a function on each keyup event and then implement filtering and debouncing from there. However, there is a better way: turn thekeyupevents into an observable stream.

RxJS provides a way to listen to events on an element usingRx.Observable.fromEvent. We can use it like so:

code/http/app/ts/components/YouTubeSearchComponent.ts

107 ngOnInit(): void {

108 // convert the `keyup` event into an observable stream

109 Observable.fromEvent(this.el.nativeElement, 'keyup') Notice that infromEvent:

• the first argument isthis.el.nativeElement (the native DOM element this component is attached to)

• the second argument is the string'keyup', which is the name of the event we want to turn into a stream

We can now perform some RxJS magic over this stream to turn it intoSearchResults. Let’s walk through step by step.

Given the stream ofkeyupevents we can chain on more methods. In the next few paragraphs we’re going to chain several functions on to our stream which will transform the stream. Then at the end we’ll show the whole example together.

1 .map((e: any) => e.target.value) // extract the value of the input

Above says, map over each keyupevent, then find the event target (e.target, that is, ourinput element) and extract thevalueof that element. This means our stream is now a stream of strings. Next:

1 .filter((text: string) => text.length > 1)

Thisfiltermeans the stream will not emit any search strings for which the length is less than one. You could set this to a higher number if you want to ignore short searches.

1 .debounceTime(250)

debounceTime means we will throttle requests that come in faster than 250ms. That is, we won’t search on every keystroke, but rather after the user has paused a small amount.

1 .do(() => this.loading.next(true)) // enable loading

Usingdoon a stream is way to perform a function mid-stream for each event, but it does not change anything in the stream. The idea here is that we’ve got our search, it has enough characters, and we’ve debounced, so now we’re about to search, so we turn onloading.

this.loadingis anEventEmitter. We “turn on”loadingby emittingtrueas the next event. We emit something on anEventEmitterby callingnext. Writingthis.loading.next(true) means, emit a trueevent on theloading EventEmitter. When we listen to theloadingevent on this component, the$eventvalue will now betrue(we’ll look more closely at using$eventbelow).

1 .map((query: string) => this.youtube.search(query)) 2 .switch()

We use .map to call perform a search for each query that is emitted. By using switch we’re, essentially, saying “ignore all search events but the most recent”1⁴³. That is, if a new search comes in, we want to use the most recent and discard the rest.

For eachquerythat comes in, we’re going to perform asearchon ourYouTubeService. Putting the chain together we have this:

⁴³ReactiveexpertswillnotethatI’mhandwavinghere.‘switch‘hasamorespecifictechnicaldefinitionwhichyoucan[readaboutintheRxJSdocshere] (https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/switch.md).

code/http/app/ts/components/YouTubeSearchComponent.ts

107 ngOnInit(): void {

108 // convert the `keyup` event into an observable stream

109 Observable.fromEvent(this.el.nativeElement, 'keyup')

110 .map((e: any) => e.target.value) // extract the value of the input

111 .filter((text: string) => text.length > 1) // filter out if empty

112 .debounceTime(250) // only once every 250ms

113 .do(() => this.loading.next(true)) // enable loading

114 // search, discarding old events if new input comes in

115 .map((query: string) => this.youtube.search(query)) 116 .switch()

117 // act on the return of the search

118 .subscribe(

The API of RxJS can be a little intimidating because the API surface area is large. That said, we’ve implemented a sophisticated event-handling stream in very few lines of code!

Because we are calling out to ourYouTubeServiceour stream is now a stream of SearchResult[]. We cansubscribeto this stream and perform actions accordingly.

subscribetakes three arguments: onSuccess, onError, onCompletion. code/http/app/ts/components/YouTubeSearchComponent.ts

118 .subscribe(

119 (results: SearchResult[]) => { // on sucesss

120 this.loading.next(false); 121 this.results.next(results);

122 },

123 (err: any) => { // on error

124 console.log(err);

125 this.loading.next(false);

126 },

127 () => { // on completion

128 this.loading.next(false);

129 }

130 );

131

132 }

The first argument specifies what we want to do when the stream emits a regular event. Here we emit an event on both of ourEventEmitters:

1. We callthis.loading.next(false), indicating we’ve stopped loading

2. We callthis.results.next(results), which will emit an event containing the list of results The second argument specifies what should happen when the stream has an event. Here we set this.loading.next(false)and log out the error.

The third argument specifies what should happen when the stream completes. Here we also emit that we’re done loading.

SearchBoxComponent: Full Listing

All together, here’s the full listing of ourSearchBoxComponent: code/http/app/ts/components/YouTubeSearchComponent.ts

88 /**

89 * SearchBox displays the search box and emits events based on the results

90 */

91

92 @Component({

93 outputs: ['loading', 'results'], 94 selector: 'search-box',

95 template: `

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

97 `

98 })

99 export class SearchBox implements OnInit {

100 loading: EventEmitter<boolean> = new EventEmitter<boolean>();

101 results: EventEmitter<SearchResult[]> = new EventEmitter<SearchResult[]>(); 102

103 constructor(public youtube: YouTubeService,

104 private el: ElementRef) {

105 }

106

107 ngOnInit(): void {

108 // convert the `keyup` event into an observable stream

109 Observable.fromEvent(this.el.nativeElement, 'keyup')

110 .map((e: any) => e.target.value) // extract the value of the input

111 .filter((text: string) => text.length > 1) // filter out if empty

112 .debounceTime(250) // only once every 250ms

113 .do(() => this.loading.next(true)) // enable loading

114 // search, discarding old events if new input comes in

115 .map((query: string) => this.youtube.search(query)) 116 .switch()

117 // act on the return of the search

118 .subscribe(

119 (results: SearchResult[]) => { // on sucesss

120 this.loading.next(false); 121 this.results.next(results);

122 },

123 (err: any) => { // on error

124 console.log(err);

125 this.loading.next(false);

126 },

127 () => { // on completion

128 this.loading.next(false);

129 } 130 ); 131 132 } 133 }

Writing

SearchResultComponent

Single Search Result Component TheSearchBoxwas pretty complicated. Let’s handle a much eas-

ier component now: theSearchResultComponent. TheSearchRe- sultComponent’s job is to render a singleSearchResult.

There’s not really any new ideas here, so let’s take it all at once: code/http/app/ts/components/YouTubeSearchComponent.ts

135 @Component({

136 inputs: ['result'],

137 selector: 'search-result', 138 template: `

139 <div class="col-sm-6 col-md-3">

140 <div class="thumbnail"> 141 <img src="{{result.thumbnailUrl}}"> 142 <div class="caption"> 143 <h3>{{result.title}}</h3> 144 <p>{{result.description}}</p> 145 <p><a href="{{result.videoUrl}}"

146 class="btn btn-default" role="button">Watch</a></p>

147 </div>

148 </div>

150 `

151 })

152 export class SearchResultComponent { 153 result: SearchResult;

154 }

A few things:

The@Componenttakes a single inputresult, on which we will put theSearchResultassigned to this component.

The templateshows the title, description, and thumbnail of the video and then links to the video via a button.

The SearchResultComponentsimply stores the SearchResultin the instance variableresult.

Writing

YouTubeSearchComponent

The last component we have to implement is theYouTubeSearch- Component. This is the component that ties everything together.

YouTubeSearchComponent @Component

code/http/app/ts/components/YouTubeSearchComponent.ts

156 @Component({

157 selector: 'youtube-search',

Our@Componentannotation is straightforward: use theselector youtube-search.

YouTubeSearchComponentController

Before we look at thetemplate, let’s take a look at theYouTube- SearchComponentcontroller:

code/http/app/ts/components/YouTubeSearchComponent.ts

187 export class YouTubeSearchComponent { 188 results: SearchResult[];

189

190 updateResults(results: SearchResult[]): void { 191 this.results = results;

192 // console.log("results:", this.results); // uncomment to take a look

193 }

194 }

This component holds one instance variable:resultswhich is an array of SearchResults.

We also define one function:updateResults.updateResultssimply takes whatever newSearchRe- sult[]it’s given and setsthis.resultsto the new value.

We’ll use bothresultsandupdateResultsin ourtemplate. YouTubeSearchComponent template

Our view needs to do three things:

1. Show the loading indicator, if we’re loading 2. Listen to events on thesearch-box

3. Show the search results

Next lets look at ourtemplate. Let’s build some basic structure and show the loading gif next to the header: code/http/app/ts/components/YouTubeSearchComponent.ts 158 template: ` 159 <div class='container'> 160 <div class="page-header"> 161 <h1>YouTube Search 162 <img 163 style="float: right;" 164 *ngIf="loading" 165 src='${loadingGif}' /> 166 </h1> 167 </div>

Notice that ourimghas asrcof ${loadingGif}- thatloadingGifvariable came from a requirestatement earlier in the program. Here we’re taking advantage of webpack’s image

loading feature. If you want to learn more about how this works, take a look at the webpack config in the sample code for this chapter or checkoutimage-webpack-loader⁴⁴.

We only want to show this loading image if loading is true, so we use ngIf to implement that functionality.

Next, let’s look at the markup where we use oursearch-box: code/http/app/ts/components/YouTubeSearchComponent.ts

168 <div class="row">

169 <div class="input-group input-group-lg col-md-12"> 170 <search-box

171 (loading)="loading = $event"

172 (results)="updateResults($event)"

173 ></search-box>

174 </div>

The interesting part here is how we bind to theloading and resultsouputs. Notice, that we use the(output)="action()"syntax here.

For theloadingoutput, we run the expressionloading = $event.$eventwill be substituted with the value of the event that is emitted from theEventEmitter. That is, in ourSearchBoxcomponent,

In document Ng Book2 r39 (Page 182-200)