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
SearchBoxTheSearchBox 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
SearchResultComponentSingle 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
YouTubeSearchComponentThe 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,