Tutorial Walk Through: Tour of Heroes in VSCode Editor – Part 3

This is  a continuation of the walk though and completion of the application.


7   HTTP

Our stakeholders appreciate our progress. Now they want to get the hero data from a server, let users add, edit, and delete heroes, and save these changes back to the server.

In this chapter we teach our application to make the corresponding HTTP calls to a remote server’s web API.

Providing HTTP Services

The HttpModule is not a core Angular module. It ships as ‘@angular/http’. This in turn is dependent on a whole set of HTTP Service. This was done deliberately, so that you as a user can pick only the elements you require for your particular application.

Adding to file: src/app/app.module.ts(v1) Making http available across components.

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 { }
Simulating the web API

We recommend registering application-wide services in the root AppModuleproviders.

Our application is in the early stages of development and far from ready for production. We don’t even have a web server that can handle requests for heroes. Until we do, we’ll have to fake it.

We’re going to trick the HTTP client into fetching and saving data from a mock service, the in-memory web API.

Here is a version of src/app/app.module.ts that performs this trick: (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 { }

You will probably at this point not have “in-memory-web-api” library.

npm i angular-in-memory-web-api

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 as follows:

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 the mock-heroes.ts which is now safe to delete.

Heroes and HTTP

Look at our current HeroService implementation

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

Let’s 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);
 }

Our updated import statements are now:

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

import 'rxjs/add/operator/toPromise';

import { Hero } from './hero';

Check in browser – ALL OK?

Ref code file:  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 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);
 }


 getHero(id: number): Promise<Hero> {
 return this.getHeroes()
 .then(heroes => heroes.find(hero => hero.id === id));
 }
}
HTTP Promise

We’re still returning a Promise but we’re creating it differently.

The Angular http.get returns an RxJS Observable. Observables are a powerful way to manage asynchronous data flows. We’ll learn about Observables later in this chapter.

For now we get back on familiar ground by immediately converting that Observable to a Promise using the toPromise operator.

Unfortunately, the Angular Observable doesn’t have a toPromise operator … not out of the box. The Angular Observable is a bare-bones implementation.

There are scores of operators like toPromise that extend Observable with useful capabilities. If we want those capabilities, we have to add the operators ourselves. That’s as easy as importing them from the RxJS library like this:

import 'rxjs/add/operator/toPromise';
Extracting the data in the then callback

In the promise’s then callback we call the json method of the HTTP Response to extract the data within the response.

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

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

Pay close attention to the shape of the data returned by the server. This particular in-memory web API example happens to return an object with a data property. Your API might return something else. Adjust the code to match your web API.

The caller is unaware of these machinations. It receives a Promise of heroes just as it did before. It has no idea that we fetched the heroes from the (mock) server. It knows nothing of the twists and turns required to convert the HTTP response into heroes. Such is the beauty and purpose of delegating data access to a service like this HeroService.

Error Handling

At the end of getHeroes() we catch server failures and pass them to an error handler:

.catch(this.handleError);

This is a critical step! We must anticipate HTTP failures as they happen frequently for reasons beyond our 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 we log the error to the console; we would do better in real life.

We’ve also decided to return a user friendly form of the 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

The HeroDetailComponent asks the HeroService to fetch a single hero to edit.

The HeroService currently fetches all heroes and then finds the desired hero by filtering for the one with the matching id. That’s fine in a simulation. It’s wasteful to ask a real server for all heroes when we only want one. Most web APIs support a get-by-id request in the form api/hero/:id (e.g., api/hero/11).

Update the HeroService.getHero method to make a get-by-id request, applying what we just learned to write getHeroes:

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);
}

It’s almost the same as getHeroes. The URL identifies which hero the server should update by encoding the hero id into the URL to match the api/hero/:id pattern.

We also adjust to the fact that the data in the response is a single hero object rather than an array.

Unchanged getHeroes API

Although we made significant internal changes to getHeroes() and getHero(), the public signatures did not change. We still return a Promise from both methods. We won’t have to update any of the components that call them.

Our stakeholders are thrilled with the web API integration so far. Now they want the ability to create and delete heroes.

Update hero details

If we try to update hero’s name, the changes are lost on clicking back button!

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 we are fetching data from a server, if we want changes to persist, we’ll need to write them back to the server.

Save hero details

Add to HeroDetailComponent (save):

In the template, html:

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

In the class:

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

And now the “Update Service” (file: src/app/hero.service.ts):

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);
}

Check in browser “Save Updated Hero”

 

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

Add a hero

To add a new hero we need to know the hero’s name. Let’s use an input element for that, paired with an add button.

Add to file: src/app/heroes.component.htm

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

And Add to heroes.component.ts

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 our array.

And on the “heroService”
Add to file: 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)
    .catch(this.handleError);
}

Check in browser

 

Delete a hero

Add to file: src/app/heroes.component.html, the delete button (just the bold)

  <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>

Add to file:src/app/heroes.component.ts  (delete method)

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; }
 });
}

Add to file:src/app/heroes.component.css (additions)

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

The delete handler method hero.service.ts

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

Check in browser!

Observables

The service currently returns an Observable of HTTP Response object, which are converted into a Promise. Many times it is best to use the Observable directly. This is a discussion visited in this link. For now this concludes our project.

 

One Reply to “Tutorial Walk Through: Tour of Heroes in VSCode Editor – Part 3”

  1. I like the valuable information you provide in your articles. I’ll bookmark your weblog and check again here frequently. I’m quite certain I’ll learn many new stuff right here! Best of luck for the next!|

Leave a Reply

Your email address will not be published. Required fields are marked *