7. HTTP

HTTP

Getting and saving data

In this page, you'll make the following improvements.

  • Get the hero data from a server.
  • Let users add, edit, and delete hero names.
  • Save the changes to the server.

You'll teach the app to make corresponding HTTP calls to a remote server's web API.

When you're done with this page, the app should look like this live example.

Where you left off

In the previous page, you learned to navigate between the dashboard and the fixed heroes list, editing a selected hero along the way. That's the starting point for this page.

Keep the app transpiling and running

Enter the following command in the terminal window:

npm start

This command runs the TypeScript compiler in "watch mode", recompiling automatically when the code changes. The command simultaneously launches the app in a browser and refreshes the browser when the code changes.

You can keep building the Tour of Heroes without pausing to recompile or refresh the browser.

Providing HTTP Services

The HttpModule is not a core Angular module. HttpModule is Angular's optional approach to web access. It exists as a separate add-on module called @angular/http and is shipped in a separate script file as part of the Angular npm package.

You're ready to import from @angular/http because systemjs.config configured SystemJS to load that library when you need it.

Register for HTTP services

The app will depend on the Angular http service, which itself depends on other supporting services. The HttpModule from the @angular/http library holds providers for a complete set of HTTP services.

To allow access to these services from anywhere in the app, add HttpModule to the imports list of the AppModule.

src/app/app.module.ts (v1)

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

import { AppRoutingModule } from './app-routing.module';

import { AppComponent }         from './app.component';
import { DashboardComponent }   from './dashboard.component';
import { HeroesComponent }      from './heroes.component';
import { HeroDetailComponent }  from './hero-detail.component';
import { HeroService }          from './hero.service';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    DashboardComponent,
    HeroDetailComponent,
    HeroesComponent,
  ],
  providers: [ HeroService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Notice that you also supply HttpModule as part of the imports array in root NgModule AppModule.

Simulate the web API

We recommend registering app-wide services in the root AppModule providers. Here you're registering in main for a special reason.

Until you have a web server that can handle requests for hero data, the HTTP client will fetch and save data from a mock service, the in-memory web API. The app itself doesn't need to know about this, so you can slip the in-memory web API into the configuration above the AppComponent.

Update src/app/app.module.ts with this version, which uses the mock service:

src/app/app.module.ts (v2)

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

import { AppRoutingModule } from './app-routing.module';

// Imports for loading & configuring the in-memory web api
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService }  from './in-memory-data.service';

import { AppComponent }         from './app.component';
import { DashboardComponent }   from './dashboard.component';
import { HeroesComponent }      from './heroes.component';
import { HeroDetailComponent }  from './hero-detail.component';
import { HeroService }          from './hero.service';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(InMemoryDataService),
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    DashboardComponent,
    HeroDetailComponent,
    HeroesComponent,
  ],
  providers: [ HeroService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Rather than require a real API server, this example simulates communication with the remote server by adding the InMemoryWebApiModule to the module imports, effectively replacing the Http client's XHR backend service with an in-memory alternative.

InMemoryWebApiModule.forRoot(InMemoryDataService),

The forRoot() configuration method takes an InMemoryDataService class that primes the in-memory database. Add the file in-memory-data.service.ts in app with the following content:

src/app/in-memory-data.service.ts

import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
  createDb() {
    let heroes = [
      {id: 11, name: 'Mr. Nice'},
      {id: 12, name: 'Narco'},
      {id: 13, name: 'Bombasto'},
      {id: 14, name: 'Celeritas'},
      {id: 15, name: 'Magneta'},
      {id: 16, name: 'RubberMan'},
      {id: 17, name: 'Dynama'},
      {id: 18, name: 'Dr IQ'},
      {id: 19, name: 'Magma'},
      {id: 20, name: 'Tornado'}
    ];
    return {heroes};
  }
}

This file replaces mock-heroes.ts, which is now safe to delete.

The in-memory web API is only useful in the early stages of development and for demonstrations such as this Tour of Heroes. Don't worry about the details of this backend substitution; you can skip it when you have a real web API server.

Read more about the in-memory web API in the Appendix: Tour of Heroes in-memory web api section of the HTTP Client page.

Heroes and HTTP

In the current HeroService implementation, a Promise resolved with mock heroes is returned.

getHeroes(): Promise<Hero[]> {
  return Promise.resolve(HEROES);
}

This was implemented in anticipation of ultimately fetching heroes with an HTTP client, which must be an asynchronous operation.

Now convert getHeroes() to use HTTP.

src/app/hero.service.ts (updated getHeroes and new class members)

  private heroesUrl = 'api/heroes';  // URL to web api

  constructor(private http: Http) { }

  getHeroes(): Promise<Hero[]> {
    return this.http.get(this.heroesUrl)
               .toPromise()
               .then(response => response.json().data as Hero[])
               .catch(this.handleError);
  }

  private handleError(error: any): Promise<any> {
    console.error('An error occurred', error); // for demo purposes only
    return Promise.reject(error.message || error);
  }

Update the import statements as follows:

src/app/hero.service.ts (updated imports)

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

import 'rxjs/add/operator/toPromise';

import { Hero } from './hero';

Refresh the browser. The hero data should successfully load from the mock server.

HTTP Promise

The Angular http.get returns an RxJS Observable. Observables are a powerful way to manage asynchronous data flows. You'll read about Observables later in this page.

For now, you've converted the Observable to a Promise using the toPromise operator.

.toPromise()

The Angular Observable doesn't have a toPromise operator out of the box.

There are many operators like toPromise that extend Observable with useful capabilities. To use those capabilities, you have to add the operators themselves. That's as easy as importing them from the RxJS library like this:

import 'rxjs/add/operator/toPromise';

You'll add more operators, and learn why you must do so, later in this tutorial.

Extracting the data in the then callback

In the Promise's then() callback, you call the json method of the HTTP Response to extract the data within the response.

.then(response => response.json().data as Hero[])

The response JSON has a single data property, which holds the array of heroes that the caller wants. So you grab that array and return it as the resolved Promise value.

Note the shape of the data that the server returns. This particular in-memory web API example returns an object with a data property. Your API might return something else. Adjust the code to match your web API.

The caller is unaware that you fetched the heroes from the (mock) server. It receives a Promise of heroes just as it did before.

Error Handling

At the end of getHeroes(), you catch server failures and pass them to an error handler.

.catch(this.handleError);

This is a critical step. You must anticipate HTTP failures, as they happen frequently for reasons beyond your control.

private handleError(error: any): Promise<any> {
  console.error('An error occurred', error); // for demo purposes only
  return Promise.reject(error.message || error);
}

In this demo service, you log the error to the console; in real life, you would handle the error in code. For a demo, this works.

The code also includes an error to the caller in a rejected promise, so that the caller can display a proper error message to the user.

Get hero by id

When the HeroDetailComponent asks the HeroService to fetch a hero, the HeroService currently fetches all heroes and filters for the one with the matching id. That's fine for a simulation, but it's wasteful to ask a real server for all heroes when you only want one. Most web APIs support a get-by-id request in the form api/hero/:id (such as api/hero/11).

Update the HeroService.getHero method to make a get-by-id request:

src/app/hero.service.ts (getHero)

getHero(id: number): Promise<Hero> {
  const url = `${this.heroesUrl}/${id}`;
  return this.http.get(url)
    .toPromise()
    .then(response => response.json().data as Hero)
    .catch(this.handleError);
}

This request is almost the same as getHeroes. The hero id in the URL identifies which hero the server should update.

Also, the data in the response is a single hero object rather than an array.

Unchanged getHeroes API

Although you made significant internal changes to getHeroes() and getHero(), the public signatures didn't change. You still return a Promise from both methods. You won't have to update any of the components that call them.

Now it's time to add the ability to create and delete heroes.

Updating hero details

Try editing a hero's name in the hero detail view. As you type, the hero name is updated in the view heading. But if you click the Back button, the changes are lost.

Updates weren't lost before. What changed? When the app used a list of mock heroes, updates were applied directly to the hero objects within the single, app-wide, shared list. Now that you're fetching data from a server, if you want changes to persist, you must write them back to the server.

Add the ability to save hero details

At the end of the hero detail template, add a save button with a click event binding that invokes a new component method named save.

src/app/hero-detail.component.html (save)

<button (click)="save()">Save</button>

Add the following save method, which persists hero name changes using the hero service update method and then navigates back to the previous view.

src/app/hero-detail.component.ts (save)

save(): void {
  this.heroService.update(this.hero)
    .then(() => this.goBack());
}

Add a hero service update method

The overall structure of the update method is similar to that of getHeroes, but it uses an HTTP put() to persist server-side changes.

src/app/hero.service.ts (update)

private headers = new Headers({'Content-Type': 'application/json'});

update(hero: Hero): Promise<Hero> {
  const url = `${this.heroesUrl}/${hero.id}`;
  return this.http
    .put(url, JSON.stringify(hero), {headers: this.headers})
    .toPromise()
    .then(() => hero)
    .catch(this.handleError);
}

To identify which hero the server should update, the hero id is encoded in the URL. The put() body is the JSON string encoding of the hero, obtained by calling JSON.stringify. The body content type (application/json) is identified in the request header.

Refresh the browser, change a hero name, save your change, and click the browser Back button. Changes should now persist.

Add the ability to add heroes

To add a hero, the app needs the hero's name. You can use an input element paired with an add button.

Insert the following into the heroes component HTML, just after the heading:

src/app/heroes.component.html (add)

<div>
  <label>Hero name:</label> <input #heroName />
  <button (click)="add(heroName.value); heroName.value=''">
    Add
  </button>
</div>

In response to a click event, call the component's click handler and then clear the input field so that it's ready for another name.

src/app/heroes.component.ts (add)

add(name: string): void {
  name = name.trim();
  if (!name) { return; }
  this.heroService.create(name)
    .then(hero => {
      this.heroes.push(hero);
      this.selectedHero = null;
    });
}

When the given name is non-blank, the handler delegates creation of the named hero to the hero service, and then adds the new hero to the array.

Implement the create method in the HeroService class.

src/app/hero.service.ts (create)

create(name: string): Promise<Hero> {
  return this.http
    .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers})
    .toPromise()
    .then(res => res.json().data as Hero)
    .catch(this.handleError);
}

Refresh the browser and create some heroes.

Add the ability to delete a hero

Each hero in the heroes view should have a delete button.

Add the following button element to the heroes component HTML, after the hero name in the repeated <li> tag.

<button class="delete"
  (click)="delete(hero); $event.stopPropagation()">x</button>

The <li> element should now look like this:

src/app/heroes.component.html (li-element)

  <li *ngFor="let hero of heroes" (click)="onSelect(hero)"
      [class.selected]="hero === selectedHero">
    <span class="badge">{{hero.id}}</span>
    <span>{{hero.name}}</span>
    <button class="delete"
      (click)="delete(hero); $event.stopPropagation()">x</button>
  </li>

In addition to calling the component's delete() method, the delete button's click handler code stops the propagation of the click event—you don't want the <li> click handler to be triggered because doing so would select the hero that the user will delete.

The logic of the delete() handler is a bit trickier:

src/app/heroes.component.ts (delete)

delete(hero: Hero): void {
  this.heroService
      .delete(hero.id)
      .then(() => {
        this.heroes = this.heroes.filter(h => h !== hero);
        if (this.selectedHero === hero) { this.selectedHero = null; }
      });
}

Of course you delegate hero deletion to the hero service, but the component is still responsible for updating the display: it removes the deleted hero from the array and resets the selected hero, if necessary.

To place the delete button at the far right of the hero entry, add this additional CSS:

src/app/heroes.component.css (additions)

button.delete {
  float:right;
  margin-top: 2px;
  margin-right: .8em;
  background-color: gray !important;
  color:white;
}

Hero service delete() method

Add the hero service's delete() method, which uses the delete() HTTP method to remove the hero from the server:

src/app/hero.service.ts (delete)

delete(id: number): Promise<void> {
  const url = `${this.heroesUrl}/${id}`;
  return this.http.delete(url, {headers: this.headers})
    .toPromise()
    .then(() => null)
    .catch(this.handleError);
}

Refresh the browser and try the new delete functionality.

Observables

Each Http service method returns an Observable of HTTP Response objects.

The HeroService converts that Observable into a Promise and returns the promise to the caller. This section shows you how, when, and why to return the Observable directly.

Background

An Observable is a stream of events that you can process with array-like operators.

Angular core has basic support for observables. Developers augment that support with operators and extensions from the RxJS library. You'll see how shortly.

Recall that the HeroService chained the toPromise operator to the Observable result of http.get(). That operator converted the Observable into a Promise and you passed that promise back to the caller.

Converting to a Promise is often a good choice. You typically ask http.get() to fetch a single chunk of data. When you receive the data, you're done. The calling component can easily consume a single result in the form of a Promise.

But requests aren't always done only once. You may start one request, cancel it, and make a different request before the server has responded to the first request. A request-cancel-new-request sequence is difficult to implement with function Promise() { [native code] }s, but easy with s.

Add the ability to search by name

You're going to add a hero search feature to the Tour of Heroes. As the user types a name into a search box, you'll make repeated HTTP requests for heroes filtered by that name.

Start by creating HeroSearchService that sends search queries to the server's web API.

src/app/hero-search.service.ts

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

import { Observable }     from 'rxjs/Observable';
import 'rxjs/add/operator/map';

import { Hero }           from './hero';

@Injectable()
export class HeroSearchService {

  constructor(private http: Http) {}

  search(term: string): Observable<Hero[]> {
    return this.http
               .get(`app/heroes/?name=${term}`)
               .map(response => response.json().data as Hero[]);
  }
}

The http.get() call in HeroSearchService is similar to the one in the HeroService, although the URL now has a query string.

More importantly, you no longer call toPromise(). Instead you return the Observable from the the htttp.get(), after chaining it to another RxJS operator, map(), to extract heroes from the response data.

RxJS operator chaining makes response processing easy and readable. See the discussion below about operators.

HeroSearchComponent

Create a HeroSearchComponent that calls the new HeroSearchService.

The component template is simple—just a text box and a list of matching search results.

src/app/hero-search.component.html

<div id="search-component">
  <h4>Hero Search</h4>
  <input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
  <div>
    <div *ngFor="let hero of heroes | async"
         (click)="gotoDetail(hero)" class="search-result" >
      {{hero.name}}
    </div>
  </div>
</div>

Also, add styles for the new component.

src/app/hero-search.component.css

.search-result{
  border-bottom: 1px solid gray;
  border-left: 1px solid gray;
  border-right: 1px solid gray;
  width:195px;
  height: 16px;
  padding: 5px;
  background-color: white;
  cursor: pointer;
}

.search-result:hover {
  color: #eee;
  background-color: #607D8B;
}

#search-box{
  width: 200px;
  height: 20px;
}

As the user types in the search box, a keyup event binding calls the component's search() method with the new search box value.

As expected, the *ngFor repeats hero objects from the component's heroes property.

But as you'll soon see, the heroes property is now an Observable of hero arrays, rather than just a hero array. The *ngFor can't do anything with an Observable until you route it through the async pipe (AsyncPipe). The async pipe subscribes to the Observable and produces the array of heroes to *ngFor.

Create the HeroSearchComponent class and metadata.

src/app/hero-search.component.ts

import { Component, OnInit } from '@angular/core';
import { Router }            from '@angular/router';

import { Observable }        from 'rxjs/Observable';
import { Subject }           from 'rxjs/Subject';

// Observable class extensions
import 'rxjs/add/observable/of';

// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';

import { HeroSearchService } from './hero-search.service';
import { Hero } from './hero';

@Component({
  selector: 'hero-search',
  templateUrl: './hero-search.component.html',
  styleUrls: [ './hero-search.component.css' ],
  providers: [HeroSearchService]
})
export class HeroSearchComponent implements OnInit {
  heroes: Observable<Hero[]>;
  private searchTerms = new Subject<string>();

  constructor(
    private heroSearchService: HeroSearchService,
    private router: Router) {}

  // Push a search term into the observable stream.
  search(term: string): void {
    this.searchTerms.next(term);
  }

  ngOnInit(): void {
    this.heroes = this.searchTerms
      .debounceTime(300)        // wait 300ms after each keystroke before considering the term
      .distinctUntilChanged()   // ignore if next search term is same as previous
      .switchMap(term => term   // switch to new observable each time the term changes
        // return the http search observable
        ? this.heroSearchService.search(term)
        // or the observable of empty heroes if there was no search term
        : Observable.of<Hero[]>([]))
      .catch(error => {
        // TODO: add real error handling
        console.log(error);
        return Observable.of<Hero[]>([]);
      });
  }

  gotoDetail(hero: Hero): void {
    let link = ['/detail', hero.id];
    this.router.navigate(link);
  }
}

Search terms

Focus on the searchTerms:

private searchTerms = new Subject<string>();

// Push a search term into the observable stream.
search(term: string): void {
  this.searchTerms.next(term);
}

A Subject is a producer of an observable event stream; searchTerms produces an Observable of strings, the filter criteria for the name search.

Each call to search() puts a new string into this subject's observable stream by calling next().

Initialize the heroes property (ngOnInit)

A Subject is also an Observable. You can turn the stream of search terms into a stream of Hero arrays and assign the result to the heroes property.

heroes: Observable<Hero[]>;

ngOnInit(): void {
  this.heroes = this.searchTerms
    .debounceTime(300)        // wait 300ms after each keystroke before considering the term
    .distinctUntilChanged()   // ignore if next search term is same as previous
    .switchMap(term => term   // switch to new observable each time the term changes
      // return the http search observable
      ? this.heroSearchService.search(term)
      // or the observable of empty heroes if there was no search term
      : Observable.of<Hero[]>([]))
    .catch(error => {
      // TODO: add real error handling
      console.log(error);
      return Observable.of<Hero[]>([]);
    });
}

Passing every user keystroke directly to the HeroSearchService would create an excessive amount of HTTP requests, taxing server resources and burning through the cellular network data plan.

Instead, you can chain Observable operators that reduce the request flow to the string Observable. You'll make fewer calls to the HeroSearchService and still get timely results. Here's how:

  • debounceTime(300) waits until the flow of new string events pauses for 300 milliseconds before passing along the latest string. You'll never make requests more frequently than 300ms.
  • distinctUntilChanged ensures that a request is sent only if the filter text changed.
  • switchMap() calls the search service for each search term that makes it through debounce and distinctUntilChanged. It cancels and discards previous search observables, returning only the latest search service observable.

With the switchMap operator (formerly known as flatMapLatest), every qualifying key event can trigger an http() method call. Even with a 300ms pause between requests, you could have multiple HTTP requests in flight and they may not return in the order sent.

switchMap() preserves the original request order while returning only the observable from the most recent http method call. Results from prior calls are canceled and discarded.

If the search text is empty, the http() method call is also short circuited and an observable containing an empty array is returned.

Note that until the service supports that feature, canceling the HeroSearchService Observable doesn't actually abort a pending HTTP request. For now, unwanted results are discarded.

  • catch intercepts a failed observable. The simple example prints the error to the console; a real life app would do better. Then to clear the search result, you return an observable containing an empty array.

Import RxJS operators

Most RxJS operators are not included in Angular's base Observable implementation. The base implementation includes only what Angular itself requires.

When you need more RxJS features, extend Observable by importing the libraries in which they are defined. Here are all the RxJS imports that this component needs:

src/app/hero-search.component.ts (rxjs imports)

import { Observable }        from 'rxjs/Observable';
import { Subject }           from 'rxjs/Subject';

// Observable class extensions
import 'rxjs/add/observable/of';

// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';

The import 'rxjs/add/...' syntax may be unfamiliar. It's missing the usual list of symbols between the braces: {...}.

You don't need the operator symbols themselves. In each case, the mere act of importing the library loads and executes the library's script file which, in turn, adds the operator to the Observable class.

Add the search component to the dashboard

Add the hero search HTML element to the bottom of the DashboardComponent template.

src/app/dashboard.component.html

<h3>Top Heroes</h3>
<div class="grid grid-pad">
  <a *ngFor="let hero of heroes"  [routerLink]="['/detail', hero.id]"  class="col-1-4">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </a>
</div>
<hero-search></hero-search>

Finally, import HeroSearchComponent from hero-search.component.ts and add it to the declarations array.

src/app/app.module.ts (search)

  declarations: [
    AppComponent,
    DashboardComponent,
    HeroDetailComponent,
    HeroesComponent,
    HeroSearchComponent
  ],

Run the app again. In the Dashboard, enter some text in the search box. If you enter characters that match any existing hero names, you'll see something like this.

Hero Search Component

App structure and code

Review the sample source code in the live example for this page. Verify that you have the following structure:

angular-tour-of-heroes
src
app
app.component.ts
app.component.css
app.module.ts
app-routing.module.ts
dashboard.component.css
dashboard.component.html
dashboard.component.ts
hero.ts
hero-detail.component.css
hero-detail.component.html
hero-detail.component.ts
hero-search.component.html (new)
hero-search.component.css (new)
hero-search.component.ts (new)
hero-search.service.ts (new)
hero.service.ts
heroes.component.css
heroes.component.html
heroes.component.ts
in-memory-data.service.ts (new)
main.ts
index.html
styles.css
systemjs.config.js
tsconfig.json
node_modules ...
package.json

Home Stretch

You're at the end of your journey, and you've accomplished a lot.

  • You added the necessary dependencies to use HTTP in the app.
  • You refactored HeroService to load heroes from a web API.
  • You extended HeroService to support post(), put(), and delete() methods.
  • You updated the components to allow adding, editing, and deleting of heroes.
  • You configured an in-memory web API.
  • You learned how to use Observables.

Here are the files you added or changed in this page.

app.comp...ts
import { Component }          from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>{{title}}</h1>
    <nav>
      <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
      <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
    </nav>
    <router-outlet></router-outlet>
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Tour of Heroes';
}
app.mod...ts
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';
import { HttpModule }    from '@angular/http';

import { AppRoutingModule } from './app-routing.module';

// Imports for loading & configuring the in-memory web api
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService }  from './in-memory-data.service';

import { AppComponent }         from './app.component';
import { DashboardComponent }   from './dashboard.component';
import { HeroesComponent }      from './heroes.component';
import { HeroDetailComponent }  from './hero-detail.component';
import { HeroService }          from './hero.service';
import { HeroSearchComponent }  from './hero-search.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(InMemoryDataService),
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    DashboardComponent,
    HeroDetailComponent,
    HeroesComponent,
    HeroSearchComponent
  ],
  providers: [ HeroService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
heroes.comp...ts
import { Component, OnInit } from '@angular/core';
import { Router }            from '@angular/router';

import { Hero }                from './hero';
import { HeroService }         from './hero.service';

@Component({
  selector: 'my-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: [ './heroes.component.css' ]
})
export class HeroesComponent implements OnInit {
  heroes: Hero[];
  selectedHero: Hero;

  constructor(
    private heroService: HeroService,
    private router: Router) { }

  getHeroes(): void {
    this.heroService
        .getHeroes()
        .then(heroes => this.heroes = heroes);
  }

  add(name: string): void {
    name = name.trim();
    if (!name) { return; }
    this.heroService.create(name)
      .then(hero => {
        this.heroes.push(hero);
        this.selectedHero = null;
      });
  }

  delete(hero: Hero): void {
    this.heroService
        .delete(hero.id)
        .then(() => {
          this.heroes = this.heroes.filter(h => h !== hero);
          if (this.selectedHero === hero) { this.selectedHero = null; }
        });
  }

  ngOnInit(): void {
    this.getHeroes();
  }

  onSelect(hero: Hero): void {
    this.selectedHero = hero;
  }

  gotoDetail(): void {
    this.router.navigate(['/detail', this.selectedHero.id]);
  }
}
heroes.comp...html
<h2>My Heroes</h2>
<div>
  <label>Hero name:</label> <input #heroName />
  <button (click)="add(heroName.value); heroName.value=''">
    Add
  </button>
</div>
<ul class="heroes">
  <li *ngFor="let hero of heroes" (click)="onSelect(hero)"
      [class.selected]="hero === selectedHero">
    <span class="badge">{{hero.id}}</span>
    <span>{{hero.name}}</span>
    <button class="delete"
      (click)="delete(hero); $event.stopPropagation()">x</button>
  </li>
</ul>
<div *ngIf="selectedHero">
  <h2>
    {{selectedHero.name | uppercase}} is my hero
  </h2>
  <button (click)="gotoDetail()">View Details</button>
</div>
heroes.comp...css
.selected {
  background-color: #CFD8DC !important;
  color: white;
}
.heroes {
  margin: 0 0 2em 0;
  list-style-type: none;
  padding: 0;
  width: 15em;
}
.heroes li {
  cursor: pointer;
  position: relative;
  left: 0;
  background-color: #EEE;
  margin: .5em;
  padding: .3em 0;
  height: 1.6em;
  border-radius: 4px;
}
.heroes li:hover {
  color: #607D8B;
  background-color: #DDD;
  left: .1em;
}
.heroes li.selected:hover {
  background-color: #BBD8DC !important;
  color: white;
}
.heroes .text {
  position: relative;
  top: -3px;
}
.heroes .badge {
  display: inline-block;
  font-size: small;
  color: white;
  padding: 0.8em 0.7em 0 0.7em;
  background-color: #607D8B;
  line-height: 1em;
  position: relative;
  left: -1px;
  top: -4px;
  height: 1.8em;
  margin-right: .8em;
  border-radius: 4px 0 0 4px;
}
button {
  font-family: Arial;
  background-color: #eee;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  cursor: hand;
}
button:hover {
  background-color: #cfd8dc;
}
button.delete {
  float:right;
  margin-top: 2px;
  margin-right: .8em;
  background-color: gray !important;
  color:white;
}
hero-detail.comp...ts
import 'rxjs/add/operator/switchMap';
import { Component, OnInit }      from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Location }               from '@angular/common';

import { Hero }        from './hero';
import { HeroService } from './hero.service';

@Component({
  selector: 'hero-detail',
  templateUrl: './hero-detail.component.html',
  styleUrls: [ './hero-detail.component.css' ]
})
export class HeroDetailComponent implements OnInit {
  hero: Hero;

  constructor(
    private heroService: HeroService,
    private route: ActivatedRoute,
    private location: Location
  ) {}

  ngOnInit(): void {
    this.route.params
      .switchMap((params: Params) => this.heroService.getHero(+params['id']))
      .subscribe(hero => this.hero = hero);
  }

  save(): void {
    this.heroService.update(this.hero)
      .then(() => this.goBack());
  }

  goBack(): void {
    this.location.back();
  }
}
hero-detail.comp...html
<div *ngIf="hero">
  <h2>{{hero.name}} details!</h2>
  <div>
    <label>id: </label>{{hero.id}}</div>
  <div>
    <label>name: </label>
    <input [(ngModel)]="hero.name" placeholder="name" />
   </div>
  <button (click)="goBack()">Back</button>
  <button (click)="save()">Save</button>
</div>
hero.service.ts
import { Injectable }    from '@angular/core';
import { Headers, Http } from '@angular/http';

import 'rxjs/add/operator/toPromise';

import { Hero } from './hero';

@Injectable()
export class HeroService {

  private headers = new Headers({'Content-Type': 'application/json'});
  private heroesUrl = 'api/heroes';  // URL to web api

  constructor(private http: Http) { }

  getHeroes(): Promise<Hero[]> {
    return this.http.get(this.heroesUrl)
               .toPromise()
               .then(response => response.json().data as Hero[])
               .catch(this.handleError);
  }


  getHero(id: number): Promise<Hero> {
    const url = `${this.heroesUrl}/${id}`;
    return this.http.get(url)
      .toPromise()
      .then(response => response.json().data as Hero)
      .catch(this.handleError);
  }

  delete(id: number): Promise<void> {
    const url = `${this.heroesUrl}/${id}`;
    return this.http.delete(url, {headers: this.headers})
      .toPromise()
      .then(() => null)
      .catch(this.handleError);
  }

  create(name: string): Promise<Hero> {
    return this.http
      .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers})
      .toPromise()
      .then(res => res.json().data as Hero)
      .catch(this.handleError);
  }

  update(hero: Hero): Promise<Hero> {
    const url = `${this.heroesUrl}/${hero.id}`;
    return this.http
      .put(url, JSON.stringify(hero), {headers: this.headers})
      .toPromise()
      .then(() => hero)
      .catch(this.handleError);
  }

  private handleError(error: any): Promise<any> {
    console.error('An error occurred', error); // for demo purposes only
    return Promise.reject(error.message || error);
  }
}
in-memory-data.service.ts
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
  createDb() {
    let heroes = [
      {id: 11, name: 'Mr. Nice'},
      {id: 12, name: 'Narco'},
      {id: 13, name: 'Bombasto'},
      {id: 14, name: 'Celeritas'},
      {id: 15, name: 'Magneta'},
      {id: 16, name: 'RubberMan'},
      {id: 17, name: 'Dynama'},
      {id: 18, name: 'Dr IQ'},
      {id: 19, name: 'Magma'},
      {id: 20, name: 'Tornado'}
    ];
    return {heroes};
  }
}
hero-search.service.ts
import { Injectable } from '@angular/core';
import { Http }       from '@angular/http';

import { Observable }     from 'rxjs/Observable';
import 'rxjs/add/operator/map';

import { Hero }           from './hero';

@Injectable()
export class HeroSearchService {

  constructor(private http: Http) {}

  search(term: string): Observable<Hero[]> {
    return this.http
               .get(`app/heroes/?name=${term}`)
               .map(response => response.json().data as Hero[]);
  }
}
hero-search.component.ts
import { Component, OnInit } from '@angular/core';
import { Router }            from '@angular/router';

import { Observable }        from 'rxjs/Observable';
import { Subject }           from 'rxjs/Subject';

// Observable class extensions
import 'rxjs/add/observable/of';

// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';

import { HeroSearchService } from './hero-search.service';
import { Hero } from './hero';

@Component({
  selector: 'hero-search',
  templateUrl: './hero-search.component.html',
  styleUrls: [ './hero-search.component.css' ],
  providers: [HeroSearchService]
})
export class HeroSearchComponent implements OnInit {
  heroes: Observable<Hero[]>;
  private searchTerms = new Subject<string>();

  constructor(
    private heroSearchService: HeroSearchService,
    private router: Router) {}

  // Push a search term into the observable stream.
  search(term: string): void {
    this.searchTerms.next(term);
  }

  ngOnInit(): void {
    this.heroes = this.searchTerms
      .debounceTime(300)        // wait 300ms after each keystroke before considering the term
      .distinctUntilChanged()   // ignore if next search term is same as previous
      .switchMap(term => term   // switch to new observable each time the term changes
        // return the http search observable
        ? this.heroSearchService.search(term)
        // or the observable of empty heroes if there was no search term
        : Observable.of<Hero[]>([]))
      .catch(error => {
        // TODO: add real error handling
        console.log(error);
        return Observable.of<Hero[]>([]);
      });
  }

  gotoDetail(hero: Hero): void {
    let link = ['/detail', hero.id];
    this.router.navigate(link);
  }
}
hero-search.component.html
<div id="search-component">
  <h4>Hero Search</h4>
  <input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
  <div>
    <div *ngFor="let hero of heroes | async"
         (click)="gotoDetail(hero)" class="search-result" >
      {{hero.name}}
    </div>
  </div>
</div>
hero-search.component.css
.search-result{
  border-bottom: 1px solid gray;
  border-left: 1px solid gray;
  border-right: 1px solid gray;
  width:195px;
  height: 16px;
  padding: 5px;
  background-color: white;
  cursor: pointer;
}

.search-result:hover {
  color: #eee;
  background-color: #607D8B;
}

#search-box{
  width: 200px;
  height: 20px;
}

Next step

Return to the learning path, where you can read more about the concepts and practices found in this tutorial.

© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v2.angular.io/docs/ts/latest/tutorial/toh-pt6.html

在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号

意见反馈
返回顶部