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.
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
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!
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);
}
}
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.
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 { }
BeersService.getBeers()
return an Observable
, one of the key classes in the RxJS library.
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).
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.
-
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); }
)
}
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.