Skip to content

Latest commit

 

History

History

step-05

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

AngularBeer - Angular tutorial - Step 05

Enough of building an app with three beers in a hard-coded dataset! Let's fetch a larger dataset from our server using one of Angular's built-in services called HttpModule. We will use Angular's dependency injection (DI) to provide the service to the beer-list.component controller.

Data

Our new dataset is now a list of 11 beers stored in JSON format in the angularbeers/src/beers/beers.json file in your project. This file is available from the server at the URL beers/beers.json

angularbeers/src/beers/beers.json:

[
  ...
  {
    "alcohol": 6.8,
    "description": "A reddish-brown abbey ale brewed with dark malts. The secondary fermentation gives a fruity aroma and a unique spicy character with a distinctive aftertaste. Secondary fermentation in the bottle.",
    "id": "AffligemDubbel",
    "img": "beers/img/AffligemDubbel.jpg",
    "name": "Affligem Dubbel"
  },
  ...
]

In order to Angular CLI be able to serve the beers JSON file, you need to add the beers folder to the static assets in the .angular-cli.json configuration file:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "project": {
    "name": "angularbeers"
  },
  "apps": [
    {
      "root": "src",
      "outDir": "dist",
      "assets": [
        "assets",
        "beers",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "main.ts",
[...]      

HttpClient

HttpClient is Angular's mechanism for communicating with a remote server over HTTP. We are going to use it to make an HTTP request to your web server to fetch the data in the beers/beers.json file.

To make HttpClient available everywhere in the app,you need to import it in the Angular module:

angularbeers/src/app/app.module.ts :

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



import { AppComponent } from './app.component';
import { BeerListComponent } from './beer-list/beer-list.component';
import { FilterArrayPipe } from './pipes/filter-array-pipe';
import { OrderByPipe } from './pipes/orderby-pipe';


@NgModule({
  declarations: [
    AppComponent,
    BeerListComponent,
    FilterArrayPipe,
    OrderByPipe
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

HttpClient is just one of several built-in modules that handle common operations in web apps. Angular injects these services for you where you need them.

Dependency injection helps to make your web apps both well-structured (e.g., separate components for presentation, data, and control) and loosely coupled (dependencies between components are not resolved by the components themselves, but by the DI subsystem).

HttpClient makes an HTTP GET request to our web server, asking for beers/beers.json (the url is relative to our index.html file). The server responds by providing the data in the JSON file. (The response might just as well have been dynamically generated by a backend server. To the browser and our app they both look the same. For the sake of simplicity we used a JSON file in this tutorial.)

HttpClient returns a Promise with a success method. We call this method to handle the asynchronous response and assign the beer data to the scope controlled by this controller, as a model called beers. Notice that Angular detected the JSON response and parsed it for us!

Create a service

Components shouldn't fetch or save data directly, they should focus on presenting data and delegate data access to a service. In this step we are creating a BeersService that all application classes can use to get beers. Instead of creating that service with new, you'll rely on Angular dependency injection to inject it into the BeerList component constructor.

So we are going to create a beers.service.ts to manage the HTTP calls and response and to give to BeerList component the list of beers.

angularbeers/src/app/services/beers.service.ts :

import {Injectable} from "@angular/core";
import { HttpClient, HttpHeaders } from '@angular/common/http';


import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { catchError, map, tap } from 'rxjs/operators';

import { Beer } from '../beer';

@Injectable()
export class BeerService {
  // URL to web API
  private beerUrl = 'beers/beers.json';

  constructor(
    private http: HttpClient) {

  }

    /** GET beers from the server */
    getBeers (): Observable<Beer[]> {
      return this.http.get<Beer[]>(this.beerUrl);
    }
}

@Injectable() services

Notice that the new service imports the Angular Injectable symbol and annotates the class with the @Injectable() decorator.

The @Injectable() decorator tells Angular that this service might itself have injected dependencies. It doesn't have dependencies now but it will soon. Whether it does or it doesn't, it's good practice to keep the decorator.

Registering the service

Then, as usual, we register the service as a provider in the Angular module.

angularbeers/src/app/app.module.ts

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



import { AppComponent } from './app.component';
import { BeerListComponent } from './beer-list/beer-list.component';
import { FilterArrayPipe } from './pipes/filter-array-pipe';
import { OrderByPipe } from './pipes/orderby-pipe';
import { BeersService } from './services/beers.service';


@NgModule({
  declarations: [
    AppComponent,
    BeerListComponent,
    FilterArrayPipe,
    OrderByPipe
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule
  ],
  providers: [
    BeersService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Observable data

BeersService.getBeers() return an Observable, one of the key classes in the RxJS library.

Inject the BeersService

We modify BeerList component definition by injecting BeersService in its constructor. Then we create a getBeers() method that use the service to recover the list of beers, and we call it at initialization by using ngOnInit().

angularbeers/src/app/beer-list/beer-list.component.ts :

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

import { BeersService } from '../services/beers.service';
import { Beer } from '../beer';

@Component({
  selector: 'beer-list',
  templateUrl: './beer-list.component.html',
  styleUrls: ['./beer-list.component.css']
})
export class BeerListComponent implements OnInit {


  orderProp : string = 'alcohol';

  beers : Array<Beer>;

  constructor(private beersService: BeersService) {
   }

  ngOnInit() {
    this.getBeers();
  }

  getBeers(): void {
    this.beersService.getBeers()
      .subscribe(beers => this.beers = beers);
  }
}

Angular's dependency injector provides services to your controller when the controller is being constructed. The dependency injector also takes care of creating any transitive dependencies the service may have (services often depend upon other services).

Adding some error control

Currently BeersService doesn't manage HTTP errors. Things go wrong, especially when you're getting data from a remote server. The BeersService.getBeers() method should catch errors and do something appropriate.

To catch errors, you "pipe" the observable result from http.get() through an RxJS catchError() operator. Extend the observable result with the .pipe() method and give it a catchError() operator.

angularbeers/src/app/app.module.ts

[...]
getBeers (): Observable<Beer[]> {
  return this.http.get<Beer[]>(this.beerUrl)
    .pipe(
      catchError(this.handleError('getBeers', []));
    );
  );
}
[...]

The catchError() operator intercepts an Observable that failed. It passes the error an error handler that can do what it wants with the error.

The following handleError() method reports the error and then returns an innocuous result so that the application keeps working. It will be shared by many HeroService methods so it's generalized to meet their different needs. Instead of handling the error directly, it returns an error handler function to catchError that it has configured with both the name of the operation that failed and a safe return value.

angularbeers/src/app/app.module.ts

[...]
/**
 * Handle Http operation that failed.
 * Let the app continue.
 * @param operation - name of the operation that failed
 * @param result - optional value to return as the observable result
 */
private handleError<T> (operation = 'operation', result?: T) {
  return (error: any): Observable<T> => {
 
    // TODO: send the error to remote logging infrastructure
    console.error(error); // log to console instead
 
    // Let the app keep running by returning an empty result.
    return of(result as T);
  };
}
[...]

After reporting the error to console, the handler constructs a user friendly message and returns a safe value to the app so it can keep working.

Because each service method returns a different kind of Observable result, errorHandler() takes a type parameter so it can return the safe value as the type that the app expects.

Experiments

  • At the bottom of angularbeers/src/app/beer-list/beer-list.component.html, add a <pre>{{beers | json}}</pre> binding to see the list of beers displayed in json format.

  • At the top of angularbeers/src/app/beer-list/beer-list.component.html, add a

<div class="alert" *ngIf="errorMessage">
  <strong>Oh snap!</strong> {{errorMessage}}.
</div>

to display an error message, you can test if by modifying the beerUrl in angularbeers/src/app/services/beers.service.ts.

In the BeerList component, pre-process the http response by limiting the number of beers to the first 5 in the list. Use the following code in the getBeers callback:

getBeers() {
    this.beerService.getBeers()
        .subscribe(
            beers => { this.beers = beers.splice(0, 5); }
        )
}

Summary

Now that you have learned how easy it is to use Angular services (thanks to Angular's dependency injection), go to step 06, where you will add some thumbnail images of beers and some links.