HTTP Client

HTTP is the primary protocol for browser/server communication.

The WebSocket protocol is another important communication technology; we won't cover it in this chapter.

Modern browsers support two HTTP-based APIs: XMLHttpRequest (XHR) and JSONP. A few browsers also support Fetch.

The Angular HTTP library simplifies application programming of the XHR and JSONP APIs as we'll learn in this chapter covering:

We illustrate these topics with code that you can run live.

Demos

This chapter describes server communication with the help of the following demos

These demos are orchestrated by the root AppComponent

app/app.component.ts

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

// Add the RxJS Observable operators we need in this app.
import './rxjs-operators';

@Component({
  selector: 'my-app',
  template: `
    <hero-list></hero-list>
    <hero-list-promise></hero-list-promise>
    <my-wiki></my-wiki>
    <my-wiki-smart></my-wiki-smart>
  `
})
export class AppComponent { }

There is nothing remarkable here except for the import of RxJS operators.

// Add the RxJS Observable operators we need in this app.
import './rxjs-operators';

We'll talk about that below when we're ready to explore observables.

First, we have to configure our application to use server communication facilities.

Providing HTTP Services

We use the Angular Http client to communicate with a server using a familiar HTTP request/response protocol. The Http client is one of a family of services in the Angular HTTP library.

SystemJS knows how to load services from the Angular HTTP library when we import from the @angular/http module because we registered that module name in the system.config file.

Before we can use the Http client , we'll have to register it as a service provider with the Dependency Injection system.

Learn about providers in the Dependency Injection chapter.

In this demo, we register providers by importing other NgModules to our root NgModule.

app/app.module.ts (v1)

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule, JsonpModule } from '@angular/http';

import { AppComponent } from './app.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    JsonpModule
  ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

We begin by importing the symbols we need, most of them familiar by now. The newcomers are the HttpModule and the JsonpModule from the Angular HTTP library.

We add these modules to the application by passing them to the imports array in our root NgModule.

We need the HttpModule to make HTTP calls. We don't need the JsonpModule for plain HTTP. We will demonstrate JSONP support later in this chapter. We're loading its module now to save time.

The Tour of Heroes HTTP Client Demo

Our first demo is a mini-version of the tutorial's "Tour of Heroes" (ToH) application. This version gets some heroes from the server, displays them in a list, lets us add new heroes, and saves them to the server. We use the Angular Http client to communicate via XMLHttpRequest (XHR).

It works like this.

ToH mini app

This demo has a single component, the HeroListComponent. Here's its template:

app/toh/hero-list.component.html (template)

<h1>Tour of Heroes ({{mode}})</h1>
<h3>Heroes:</h3>
<ul>
  <li *ngFor="let hero of heroes">
    {{hero.name}}
  </li>
</ul>
New hero name:
<input #newHeroName />
<button (click)="addHero(newHeroName.value); newHeroName.value=''">
  Add Hero
</button>
<div class="error" *ngIf="errorMessage">{{errorMessage}}</div>

It presents the list of heroes with an ngFor. Below the list is an input box and an Add Hero button where we can enter the names of new heroes and add them to the database. We use a template reference variable, newHeroName, to access the value of the input box in the (click) event binding. When the user clicks the button, we pass that value to the component's addHero method and then clear it to make it ready for a new hero name.

Below the button is an area for an error message.

The HeroListComponent class

Here's the component class:

app/toh/hero-list.component.ts (class)

export class HeroListComponent implements OnInit {
  errorMessage: string;
  heroes: Hero[];
  mode = 'Observable';

  constructor (private heroService: HeroService) {}

  ngOnInit() { this.getHeroes(); }

  getHeroes() {
    this.heroService.getHeroes()
                     .subscribe(
                       heroes => this.heroes = heroes,
                       error =>  this.errorMessage = <any>error);
  }

  addHero (name: string) {
    if (!name) { return; }
    this.heroService.addHero(name)
                     .subscribe(
                       hero  => this.heroes.push(hero),
                       error =>  this.errorMessage = <any>error);
  }
}

Angular injects a HeroService into the constructor and the component calls that service to fetch and save data.

The component does not talk directly to the Angular Http client! The component doesn't know or care how we get the data. It delegates to the HeroService.

This is a golden rule: always delegate data access to a supporting service class.

Although at runtime the component requests heroes immediately after creation, we do not call the service's get method in the component's constructor. We call it inside the ngOnInit lifecycle hook instead and count on Angular to call ngOnInit when it instantiates this component.

This is a best practice. Components are easier to test and debug when their constructors are simple and all real work (especially calling a remote server) is handled in a separate method.

The service's getHeroes() and addHero() methods return an Observable of hero data that the Angular Http client fetched from the server.

Observables are a big topic, beyond the scope of this chapter. But we need to know a little about them to appreciate what is going on here.

We should think of an Observable as a stream of events published by some source. We listen for events in this stream by subscribing to the Observable. In these subscriptions we specify the actions to take when the web request produces a success event (with the hero data in the event payload) or a fail event (with the error in the payload).

With our basic intuitions about the component squared away, we're ready to look inside the HeroService.

Fetch data with the HeroService

In many of our previous samples we faked the interaction with the server by returning mock heroes in a service like this one:

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

import { Hero } from './hero';
import { HEROES } from './mock-heroes';

@Injectable()
export class HeroService {
  getHeroes(): Promise<Hero[]> {
    return Promise.resolve(HEROES);
  }
}

In this chapter, we revise that HeroService to get the heroes from the server using the Angular Http client service:

app/toh/hero.service.ts (revised)

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

import { Hero }           from './hero';
import { Observable }     from 'rxjs/Observable';

@Injectable()
export class HeroService {
  constructor (private http: Http) {}

  private heroesUrl = 'app/heroes';  // URL to web API

  getHeroes (): Observable<Hero[]> {
    return this.http.get(this.heroesUrl)
                    .map(this.extractData)
                    .catch(this.handleError);
  }
  private extractData(res: Response) {
    let body = res.json();
    return body.data || { };
  }

  private handleError (error: any) {
    // In a real world app, we might use a remote logging infrastructure
    // We'd also dig deeper into the error to get a better message
    let errMsg = (error.message) ? error.message :
      error.status ? `${error.status} - ${error.statusText}` : 'Server error';
    console.error(errMsg); // log to console instead
    return Observable.throw(errMsg);
  }
}

Notice that the Angular Http client service is injected into the HeroService constructor.

constructor (private http: Http) {}

Look closely at how we call http.get

app/toh/hero.service.ts (getHeroes)

getHeroes (): Observable<Hero[]> {
  return this.http.get(this.heroesUrl)
                  .map(this.extractData)
                  .catch(this.handleError);
}

We pass the resource URL to get and it calls the server which should return heroes.

It will return heroes once we've set up the in-memory web api described in the appendix below. Alternatively, we can (temporarily) target a JSON file by changing the endpoint URL:

private heroesUrl = 'app/heroes.json'; // URL to JSON file

The return value may surprise us. Many of us who are familiar with asynchronous methods in modern JavaScript would expect the get method to return a promise. We'd expect to chain a call to then() and extract the heroes. Instead we're calling a map() method. Clearly this is not a promise.

In fact, the http.get method returns an Observable of HTTP Responses (Observable<Response>) from the RxJS library and map is one of the RxJS operators.

RxJS Library

RxJS ("Reactive Extensions") is a 3rd party library, endorsed by Angular, that implements the asynchronous observable pattern.

All of our Developer Guide samples have installed the RxJS npm package and loaded via system.js because observables are used widely in Angular applications. We certainly need it now when working with the HTTP client. And we must take a critical extra step to make RxJS observables usable.

Enable RxJS Operators

The RxJS library is quite large. Size matters when we build a production application and deploy it to mobile devices. We should include only those features that we actually need.

Accordingly, Angular exposes a stripped down version of Observable in the rxjs/Observable module, a version that lacks most of the operators including some we'd like to use here such as the map method we called above in getHeroes.

It's up to us to add the operators we need.

We could add every RxJS operators with a single import statement. While that is the easiest thing to do, we'd pay a penalty in extended launch time and application size because the full library is so big. We only use a few operators in our app.

Instead, we'll import each Observable operator and static class method, one-by-one, until we have a custom Observable implementation tuned precisely to our requirements. We'll put the import statements in one app/rxjs-operators.ts file.

app/rxjs-operators.ts

// import 'rxjs/Rx'; // adds ALL RxJS statics & operators to Observable

// See node_module/rxjs/Rxjs.js
// Import just the rxjs statics and operators we need for THIS app.

// Statics
import 'rxjs/add/observable/throw';

// Operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/toPromise';

If we forget an operator, the TypeScript compiler will warn that it's missing and we'll update this file.

We don't need all of these particular operators in the HeroService — just map, catch and throw. We'll need the other operators later, in a Wiki example below.

Finally, we import rxjs-operatoritself in our app.component.ts:

app/app.component.ts (import rxjs)

// Add the RxJS Observable operators we need in this app.
import './rxjs-operators';

Let's return to our study of the HeroService.

Process the response object

Remember that our getHeroes() method mapped the http.get response object to heroes with an extractData helper method:

app/toh/hero.service.ts (excerpt)

private extractData(res: Response) {
  let body = res.json();
  return body.data || { };
}

The response object does not hold our data in a form we can use directly. To make it useful in our application we must parse the response data into a JSON object

Parse to JSON

The response data are in JSON string form. We must parse that string into JavaScript objects which we do by calling response.json().

This is not Angular's own design. The Angular HTTP client follows the Fetch specification for the response object returned by the Fetch function. That spec defines a json() method that parses the response body into a JavaScript object.

We shouldn't expect the decoded JSON to be the heroes array directly. The server we're calling always wraps JSON results in an object with a data property. We have to unwrap it to get the heroes. This is conventional web api behavior, driven by security concerns.

Make no assumptions about the server API. Not all servers return an object with a data property.

Do not return the response object

Our getHeroes() could have returned the HTTP response. Bad idea! The point of a data service is to hide the server interaction details from consumers. The component that calls the HeroService wants heroes. It has no interest in what we do to get them. It doesn't care where they come from. And it certainly doesn't want to deal with a response object.

HTTP GET is delayed

The http.get does not send the request just yet! This observable is cold which means the request won't go out until something subscribes to the observable. That something is the HeroListComponent.

Always handle errors

Whenever we deal with I/O we must be prepared for something to go wrong as it surely will. We should catch errors in the HeroService and do something with them. We may also pass an error message back to the component for presentation to the user but only if we can say something the user can understand and act upon.

In this simple app we provide rudimentary error handling in both the service and the component.

The eagle-eyed reader may have spotted our use of the catch operator in conjunction with a handleError method. We haven't discussed so far how that actually works.

We use the Observable catch operator on the service level. It takes an error handling function with an error object as the argument. Our service handler, handleError, logs the response to the console, transforms the error into a user-friendly message, and returns the message in a new, failed observable via Observable.throw.

app/toh/hero.service.ts (excerpt)

getHeroes (): Observable<Hero[]> {
  return this.http.get(this.heroesUrl)
                  .map(this.extractData)
                  .catch(this.handleError);
}
private handleError (error: any) {
  // In a real world app, we might use a remote logging infrastructure
  // We'd also dig deeper into the error to get a better message
  let errMsg = (error.message) ? error.message :
    error.status ? `${error.status} - ${error.statusText}` : 'Server error';
  console.error(errMsg); // log to console instead
  return Observable.throw(errMsg);
}

HeroListComponent error handling

Back in the HeroListComponent, where we called heroService.getHeroes(), we supply the subscribe function with a second function parameter to handle the error message. It sets an errorMessage variable which we've bound conditionally in the HeroListComponent template.

app/toh/hero-list.component.ts (getHeroes)

getHeroes() {
  this.heroService.getHeroes()
                   .subscribe(
                     heroes => this.heroes = heroes,
                     error =>  this.errorMessage = <any>error);
}

Want to see it fail? Reset the api endpoint in the HeroService to a bad value. Remember to restore it!

Send data to the server

So far we've seen how to retrieve data from a remote location using an HTTP service. Let's add the ability to create new heroes and save them in the backend.

We'll create an easy method for the HeroListComponent to call, an addHero() method that takes just the name of a new hero:

addHero (name: string): Observable<Hero> {

To implement it, we need to know some details about the server's api for creating heroes.

Our data server follows typical REST guidelines. It expects a POST request at the same endpoint where we GET heroes. It expects the new hero data to arrive in the body of the request, structured like a Hero entity but without the id property. The body of the request should look like this:

{ "name": "Windstorm" }

The server will generate the id and return the entire JSON representation of the new hero including its generated id. The hero arrives tucked inside a response object with its own data property.

Now that we know how the API works, we implement addHero()like this:

app/toh/hero.service.ts (additional imports)

import { Headers, RequestOptions } from '@angular/http';

app/toh/hero.service.ts (addHero)

  addHero (name: string): Observable<Hero> {
    let body = JSON.stringify({ name });
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });

    return this.http.post(this.heroesUrl, body, options)
                    .map(this.extractData)
                    .catch(this.handleError);
  }

Headers

The Content-Type header allows us to inform the server that the body will represent JSON.

Headers are one of the RequestOptions. Compose the options object and pass it in as the third parameter of the post method, as shown above.

Body

Despite the content type being specified as JSON, the POST body must actually be a string. Hence, we explicitly encode the JSON hero content before passing it in as the body argument.

We may be able to skip the JSON.stringify step in the near future.

JSON results

As with getHeroes(), we extract the data from the response using the extractData() helper.

Back in the HeroListComponent, we see that its addHero() method subscribes to the observable returned by the service's addHero() method. When the data, arrive it pushes the new hero object into its heroes array for presentation to the user.

app/toh/hero-list.component.ts (addHero)

addHero (name: string) {
  if (!name) { return; }
  this.heroService.addHero(name)
                   .subscribe(
                     hero  => this.heroes.push(hero),
                     error =>  this.errorMessage = <any>error);
}

Fall back to Promises

Although the Angular http client API returns an Observable<Response> we can turn it into a Promise<Response> if we prefer. It's easy to do and a promise-based version looks much like the observable-based version in simple cases.

While promises may be more familiar, observables have many advantages. Don't rush to promises until you give observables a chance.

Let's rewrite the HeroService using promises , highlighting just the parts that are different.

app/toh/hero.service.promise.ts (promise-based)
getHeroes (): Promise<Hero[]> {
  return this.http.get(this.heroesUrl)
                  .toPromise()
                  .then(this.extractData)
                  .catch(this.handleError);
}

addHero (name: string): Promise<Hero> {
  let body = JSON.stringify({ name });
  let headers = new Headers({ 'Content-Type': 'application/json' });
  let options = new RequestOptions({ headers: headers });

  return this.http.post(this.heroesUrl, body, options)
             .toPromise()
             .then(this.extractData)
             .catch(this.handleError);
}

private extractData(res: Response) {
  let body = res.json();
  return body.data || { };
}

private handleError (error: any) {
  // In a real world app, we might use a remote logging infrastructure
  // We'd also dig deeper into the error to get a better message
  let errMsg = (error.message) ? error.message :
    error.status ? `${error.status} - ${error.statusText}` : 'Server error';
  console.error(errMsg); // log to console instead
  return Promise.reject(errMsg);
}
app/toh/hero.service.ts (observable-based)
  getHeroes (): Observable<Hero[]> {
    return this.http.get(this.heroesUrl)
                    .map(this.extractData)
                    .catch(this.handleError);
  }

  addHero (name: string): Observable<Hero> {
    let body = JSON.stringify({ name });
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });

    return this.http.post(this.heroesUrl, body, options)
                    .map(this.extractData)
                    .catch(this.handleError);
  }

  private extractData(res: Response) {
    let body = res.json();
    return body.data || { };
  }

  private handleError (error: any) {
    // In a real world app, we might use a remote logging infrastructure
    // We'd also dig deeper into the error to get a better message
    let errMsg = (error.message) ? error.message :
      error.status ? `${error.status} - ${error.statusText}` : 'Server error';
    console.error(errMsg); // log to console instead
    return Observable.throw(errMsg);
  }

Converting from an observable to a promise is as simple as calling toPromise(success, fail).

We move the observable's map callback to the first success parameter and its catch callback to the second fail parameter and we're done! Or we can follow the promise then.catch pattern as we do in the second addHero example.

Our errorHandler forwards an error message as a failed promise instead of a failed Observable.

The diagnostic log to console is just one more then in the promise chain.

We have to adjust the calling component to expect a Promise instead of an Observable.

app/toh/hero-list.component.promise.ts (promise-based)
getHeroes() {
  this.heroService.getHeroes()
                   .then(
                     heroes => this.heroes = heroes,
                     error =>  this.errorMessage = <any>error);
}

addHero (name: string) {
  if (!name) { return; }
  this.heroService.addHero(name)
                   .then(
                     hero  => this.heroes.push(hero),
                     error =>  this.errorMessage = <any>error);
}
app/toh/hero-list.component.ts (observable-based)
  getHeroes() {
    this.heroService.getHeroes()
                     .subscribe(
                       heroes => this.heroes = heroes,
                       error =>  this.errorMessage = <any>error);
  }

  addHero (name: string) {
    if (!name) { return; }
    this.heroService.addHero(name)
                     .subscribe(
                       hero  => this.heroes.push(hero),
                       error =>  this.errorMessage = <any>error);
  }

The only obvious difference is that we call then on the returned promise instead of subscribe. We give both methods the same functional arguments.

The less obvious but critical difference is that these two methods return very different results!

The promise-based then returns another promise. We can keep chaining more then and catch calls, getting a new promise each time.

The subscribe method returns a Subscription. A Subscription is not another Observable. It's the end of the line for observables. We can't call map on it or call subscribe again. The Subscription object has a different purpose, signified by its primary method, unsubscribe.

Learn more about observables to understand the implications and consequences of subscriptions.

Cross-origin requests: Wikipedia example

We just learned how to make XMLHttpRequests using the Angular Http service. This is the most common approach for server communication. It doesn't work in all scenarios.

For security reasons, web browsers block XHR calls to a remote server whose origin is different from the origin of the web page. The origin is the combination of URI scheme, hostname and port number. This is called the Same-origin Policy.

Modern browsers do allow XHR requests to servers from a different origin if the server supports the CORS protocol. If the server requires user credentials, we'll enable them in the request headers.

Some servers do not support CORS but do support an older, read-only alternative called JSONP. Wikipedia is one such server.

This StackOverflow answer covers many details of JSONP.

Search wikipedia

Let's build a simple search that shows suggestions from wikipedia as we type in a text box.

Wikipedia search app (v.1)

Wikipedia offers a modern CORS API and a legacy JSONP search API. Let's use the latter for this example. The Angular Jsonp service both extends the Http service for JSONP and restricts us to GET requests. All other HTTP methods throw an error because JSONP is a read-only facility.

As always, we wrap our interaction with an Angular data access client service inside a dedicated service, here called WikipediaService.

app/wiki/wikipedia.service.ts

import { Injectable } from '@angular/core';
import { Jsonp, URLSearchParams } from '@angular/http';

@Injectable()
export class WikipediaService {
  constructor(private jsonp: Jsonp) {}

  search (term: string) {

    let wikiUrl = 'http://en.wikipedia.org/w/api.php';

    let params = new URLSearchParams();
    params.set('search', term); // the user's search value
    params.set('action', 'opensearch');
    params.set('format', 'json');
    params.set('callback', 'JSONP_CALLBACK');

    // TODO: Add error handling
    return this.jsonp
               .get(wikiUrl, { search: params })
               .map(response => <string[]> response.json()[1]);
  }
}

The constructor expects Angular to inject its jsonp service. We made that service available by importing the JsonpModule into our root NgModule.

Search parameters

The Wikipedia 'opensearch' API expects four parameters (key/value pairs) to arrive in the request URL's query string. The keys are search, action, format, and callback. The value of the search key is the user-supplied search term to find in Wikipedia. The other three are the fixed values "opensearch", "json", and "JSONP_CALLBACK" respectively.

The JSONP technique requires that we pass a callback function name to the server in the query string: callback=JSONP_CALLBACK. The server uses that name to build a JavaScript wrapper function in its response which Angular ultimately calls to extract the data. All of this happens under the hood.

If we're looking for articles with the word "Angular", we could construct the query string by hand and call jsonp like this:

let queryString =
  `?search=${term}&action=opensearch&format=json&callback=JSONP_CALLBACK`;

return this.jsonp
           .get(wikiUrl + queryString)
           .map(response => <string[]> response.json()[1]);

In more parameterized examples we might prefer to build the query string with the Angular URLSearchParams helper as shown here:

app/wiki/wikipedia.service.ts (search parameters)

let params = new URLSearchParams();
params.set('search', term); // the user's search value
params.set('action', 'opensearch');
params.set('format', 'json');
params.set('callback', 'JSONP_CALLBACK');

This time we call jsonp with two arguments: the wikiUrl and an options object whose search property is the params object.

app/wiki/wikipedia.service.ts (call jsonp)

// TODO: Add error handling
return this.jsonp
           .get(wikiUrl, { search: params })
           .map(response => <string[]> response.json()[1]);

Jsonp flattens the params object into the same query string we saw earlier before putting the request on the wire.

The WikiComponent

Now that we have a service that can query the Wikipedia API, we turn to the component that takes user input and displays search results.

app/wiki/wiki.component.ts

import { Component }        from '@angular/core';
import { Observable }       from 'rxjs/Observable';

import { WikipediaService } from './wikipedia.service';

@Component({
  selector: 'my-wiki',
  template: `
    <h1>Wikipedia Demo</h1>
    <p><i>Fetches after each keystroke</i></p>

    <input #term (keyup)="search(term.value)"/>

    <ul>
      <li *ngFor="let item of items | async">{{item}}</li>
    </ul>
  `,
  providers: [WikipediaService]
})
export class WikiComponent {
  items: Observable<string[]>;

  constructor (private wikipediaService: WikipediaService) {}

  search (term: string) {
    this.items = this.wikipediaService.search(term);
  }
}

The component presents an <input> element search box to gather search terms from the user. and calls a search(term) method after each keyup event.

The search(term) method delegates to our WikipediaService which returns an observable array of string results (Observable<string[]>). Instead of subscribing to the observable inside the component as we did in the HeroListComponent, we forward the observable result to the template (via items) where the async pipe in the ngFor handles the subscription.

We often use the async pipe in read-only components where the component has no need to interact with the data. We couldn't use the pipe in the HeroListComponent because the "add hero" feature pushes newly created heroes into the list.

Our wasteful app

Our wikipedia search makes too many calls to the server. It is inefficient and potentially expensive on mobile devices with limited data plans.

1. Wait for the user to stop typing

At the moment we call the server after every key stroke. The app should only make requests when the user stops typing . Here's how it should work — and will work — when we're done refactoring:

Wikipedia search app (v.2)

2. Search when the search term changes

Suppose the user enters the word angular in the search box and pauses for a while. The application issues a search request for Angular.

Then the user backspaces over the last three letters, lar, and immediately re-types lar before pausing once more. The search term is still "angular". The app shouldn't make another request.

3. Cope with out-of-order responses

The user enters angular, pauses, clears the search box, and enters http. The application issues two search requests, one for angular and one for http.

Which response will arrive first? We can't be sure. A load balancer could dispatch the requests to two different servers with different response times. The results from the first angular request might arrive after the later http results. The user will be confused if we display the angular results to the http query.

When there are multiple requests in-flight, the app should present the responses in the original request order. That won't happen if angular results arrive last.

More fun with Observables

We can address these problems and improve our app with the help of some nifty observable operators.

We could make our changes to the WikipediaService. But we sense that our concerns are driven by the user experience so we update the component class instead.

app/wiki/wiki-smart.component.ts

import { Component }        from '@angular/core';
import { Observable }       from 'rxjs/Observable';
import { Subject }          from 'rxjs/Subject';

import { WikipediaService } from './wikipedia.service';

@Component({
  selector: 'my-wiki-smart',
  template: `
    <h1>Smarter Wikipedia Demo</h1>
    <p><i>Fetches when typing stops</i></p>

    <input #term (keyup)="search(term.value)"/>

    <ul>
      <li *ngFor="let item of items | async">{{item}}</li>
    </ul>
  `,
  providers: [WikipediaService]
})
export class WikiSmartComponent {

  constructor (private wikipediaService: WikipediaService) { }

  private searchTermStream = new Subject<string>();

  search(term: string) { this.searchTermStream.next(term); }

  items: Observable<string[]> = this.searchTermStream
    .debounceTime(300)
    .distinctUntilChanged()
    .switchMap((term: string) => this.wikipediaService.search(term));
}

We made no changes to the template or metadata, confining them all to the component class. Let's review those changes.

Create a stream of search terms

We're binding to the search box keyup event and calling the component's search method after each keystroke.

We turn these events into an observable stream of search terms using a Subject which we import from the RxJS observable library:

import { Subject }          from 'rxjs/Subject';

Each search term is a string, so we create a new Subject of type string called searchTermStream. After every keystroke, the search method adds the search box value to that stream via the subject's next method.

private searchTermStream = new Subject<string>();

search(term: string) { this.searchTermStream.next(term); }

Listen for search terms

Earlier, we passed each search term directly to the service and bound the template to the service results. Now we listen to the stream of terms, manipulating the stream before it reaches the WikipediaService.

items: Observable<string[]> = this.searchTermStream
  .debounceTime(300)
  .distinctUntilChanged()
  .switchMap((term: string) => this.wikipediaService.search(term));

We wait for the user to stop typing for at least 300 milliseconds (debounceTime). Only changed search values make it through to the service (distinctUntilChanged).

The WikipediaService returns a separate observable of string arrays (Observable<string[]>) for each request. We could have multiple requests in flight, all awaiting the server's reply, which means multiple observables-of-strings could arrive at any moment in any order.

The switchMap (formerly known as flatMapLatest) returns a new observable that combines these WikipediaService observables, re-arranges them in their original request order, and delivers to subscribers only the most recent search results.

The displayed list of search results stays in sync with the user's sequence of search terms.

We added the debounceTime, distinctUntilChanged, and switchMap operators to the RxJS Observable class in rxjs-operators as described above

Appendix: Tour of Heroes in-memory server

If we only cared to retrieve data, we could tell Angular to get the heroes from a heroes.json file like this one:

app/heroes.json

{
  "data": [
    { "id": "1", "name": "Windstorm" },
    { "id": "2", "name": "Bombasto" },
    { "id": "3", "name": "Magneta" },
    { "id": "4", "name": "Tornado" }
  ]
}

We wrap the heroes array in an object with a data property for the same reason that a data server does: to mitigate the security risk posed by top-level JSON arrays.

We'd set the endpoint to the JSON file like this:

private heroesUrl = 'app/heroes.json'; // URL to JSON file

The get heroes scenario would work. But we want to save data too. We can't save changes to a JSON file. We need a web API server. We didn't want the hassle of setting up and maintaining a real server for this chapter. So we turned to an in-memory web API simulator instead.

The in-memory web api is not part of the Angular core. It's an optional service in its own angular2-in-memory-web-api library that we installed with npm (see package.json) and registered for module loading by SystemJS (see systemjs.config.js)

The in-memory web API gets its data from a custom application class with a createDb() method that returns a map whose keys are collection names and whose values are arrays of objects in those collections.

Here's the class we created for this sample based on the JSON data:

app/hero-data.ts

import { InMemoryDbService } from 'angular2-in-memory-web-api';
export class HeroData implements InMemoryDbService {
  createDb() {
    let heroes = [
      { id: '1', name: 'Windstorm' },
      { id: '2', name: 'Bombasto' },
      { id: '3', name: 'Magneta' },
      { id: '4', name: 'Tornado' }
    ];
    return {heroes};
  }
}

Ensure that the HeroService endpoint refers to the web API:

private heroesUrl = 'app/heroes';  // URL to web API

Finally, redirect client HTTP requests to the in-memory web API.

This redirection is easy to configure with the in-memory web API service module by adding the InMemoryWebApiModule to the AppModule.imports list. At the same time, we're calling its forRoot configuration method with the HeroData class.

InMemoryWebApiModule.forRoot(HeroData)

How it works

Angular's http service delegates the client/server communication tasks to a helper service called the XHRBackend.

Using standard Angular provider registration techniques, the InMemoryWebApiModule replaces the default XHRBackend service with its own in-memory alternative. The forRoot method initialize the in-memory web API with seed data from the mock hero dataset at the same time.

The forRoot method name is a strong reminder that you should only call the InMemoryWebApiModule once while setting the metadata for the root AppModule. Don't call it again!.

Here is the revised (and final) version of app/app.module.ts demonstrating these steps.

app/app.module.ts (excerpt)

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule, JsonpModule }  from '@angular/http';

import { InMemoryWebApiModule }     from 'angular2-in-memory-web-api';
import { HeroData }                 from './hero-data';

import { AppComponent }             from './app.component';

import { HeroListComponent }        from './toh/hero-list.component';
import { HeroListPromiseComponent } from './toh/hero-list.component.promise';

import { WikiComponent }      from './wiki/wiki.component';
import { WikiSmartComponent } from './wiki/wiki-smart.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    JsonpModule,
    InMemoryWebApiModule.forRoot(HeroData)
  ],
  declarations: [
    AppComponent,
    HeroListComponent,
    HeroListPromiseComponent,
    WikiComponent,
    WikiSmartComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

See the full source code in the live example.

doc_Angular
2016-10-06 09:46:36
Comments
Leave a Comment

Please login to continue.