Chapter 12. Interacting with servers using HTTP

This chapter covers

  • Working with the HttpClient service
  • Creating a simple web server using the Node and Express frameworks
  • Developing an Angular client that communicates with the Node server
  • Intercepting HTTP requests and responses

Angular applications can communicate with any web server supporting HTTP, regardless of what server-side platform is used. In this chapter, we’ll show you how to use the HttpClient service offered by Angular. You’ll see how to make HTTP GET and POST methods with HttpClient. And you’ll learn how to intercept HTTP requests to implement cross-cutting concerns, such as global error handling.

This chapter starts with a brief overview of Angular’s HttpClient service, and then you’ll create a web server using the Node.js and Express.js frameworks. The server will serve the data required for most code samples in this chapter.

Finally, you’ll see how to implement HTTP interceptors and report progress while transferring large assets.

12.1. Overview of the HttpClient service

Browser-based web apps run HTTP requests asynchronously, so the UI remains responsive, and the user can continue working with the application while HTTP requests are being processed by the server. Asynchronous HTTP requests can be implemented using callbacks, promises, or observables. Although promises allow you to move away from callback hell (see section A.12.2 in appendix A), they have the following shortcomings:

  • There’s no way to cancel a pending request made with a promise.
  • When a promise resolves or rejects, the client receives either data or an error message, but in both cases it’ll be a single piece of data. A JavaScript promise doesn’t offer a way to handle a continuous stream of data chunks delivered over time.

Observables don’t have these shortcomings. In section 6.4 in chapter 6, we demonstrated how you can cancel HTTP requests made with observables, and in chapter 13, you’ll see how a server can push a stream of data to the client using WebSockets.

Angular supports HTTP communications via the HttpClient service from the @angular/common/http package. If your app requires HTTP communications, you need to add HttpClientModule to the imports section of the @NgModule() decorator.

If you peek inside the type definition file @angular/common/http/src/client.d.ts, you’ll see that get(), post(), put(), delete(), and many other methods return an Observable, and an app needs to subscribe to get the data. To use the HttpClient service, you need to inject it into a service or component.

Note

As explained in chapter 5, every injectable service requires a provider declaration. The providers for HttpClient are declared in HttpClientModule, so you don’t need to declare them in your app.

The following listing illustrates one way of invoking the get() method of the HttpClient service, passing a URL as a string. You retrieve products of type Product here.

Listing 12.1. Making an HTTP GET request
interface Product {                                                       1
     id: number,
    title: string
}
...
constructor(private httpClient: HttpClient) { }                           2

ngOnInit() {
  this.httpClient.get<Product>('/product/123')                            3
       .subscribe(
        data => console.log(`id: ${data.id} title: ${data.title}`),       4
         (err: HttpErrorResponse) => console.log(`Got error: ${err}`)     5
       );
}

  • 1 Defines the type Product
  • 2 Injects the HttpClient service
  • 3 Declares a get() request
  • 4 Subscribes to the result of get()
  • 5 Logs an error, if any

In the get() method, you haven’t specified the full URL (such as http://localhost:8000/product) assuming that the Angular app makes a request to the same server where it was deployed, so the base portion of the URL can be omitted. Note that in get<Product>(), you use TypeScript generics (see section B.9 in appendix B) to specify the type of data expected in the body of the HTTP response. The type annotation doesn’t enforce or validate the shape of the data returned by the server; it just makes the other code aware of the expected server response. By default, the response type is any, and the TypeScript compiler won’t be able to type-check the properties you access on the returned object.

Your subscribe() method receives and prints the data on the browser’s console. By default, HttpClient expects the data in JSON format, and the data is automatically converted into JavaScript objects. If you expect non-JSON data, use the responseType option. For example, you can read arbitrary text from a file as shown in the following listing.

Listing 12.2. Specifying string as a returned data type
let someData: string;
this.httpClient
    .get<string>('/my_data_file.txt', {responseType: 'text'})            1
     .subscribe(
        data => someData = data,                                         2
         (err: HttpErrorResponse) => console.log(`Got error: ${err}`)    3
      );

  • 1 Specifies string as a response body type
  • 2 Assigns the received data to a variable
  • 3 Logs errors, if any
Tip

The post(), put(), and delete() methods are used in a fashion similar to listing 12.2 by invoking one of these methods and subscribing to the results.

Now let’s create an app that reads some data from a JSON file.

12.2. Reading a JSON file with HttpClient

To illustrate HttpClient.get(), your app will read a file containing JSON-formatted product data. Create a new folder that contains the products.json file shown in the following listing.

Listing 12.3. The file data/products.json
[
  { "id": 0, "title": "First Product", "price": 24.99 },
  { "id": 1, "title": "Second Product", "price": 64.99 },
  { "id": 2, "title": "Third Product", "price": 74.99}
]

The folder data and the products.json file become assets of your app that need to be included in the project bundles, so you’ll add this folder to the app’s assets property in the .angular-cli.json file (or angular.json, starting from Angular 6), as shown in the next listing.

Listing 12.4. A fragment from .angular-cli.json
"assets": [
  "assets",      1
   "data"        2
 ],

  • 1 The name of the default assets folder generated by Angular CLI
  • 2 The name of the folder with assets you add to the project

Let’s create an app that will show the product data, as shown in figure 12.1.

Figure 12.1. Rendering the content of products.json

Your app component will use HttpClient.get() to issue an HTTP GET request, and you’ll declare a Product interface defining the structure of the expected product data. The observable returned by get() will be unwrapped in the template by the async pipe. The app.component.ts file is located in the readfile folder and has the content shown in the following listing.

Listing 12.5. app.component.ts
interface Product {                                                   1
   id: string;
  title: string;
  price: number;
}

@Component({
  selector: 'app-root',
  template: `<h1>Products</h1>
  <ul>
    <li *ngFor="let product of products$ | async">                    2
       {{product.title }}: {{product.price | currency}}               3
     </li>
  </ul>
  `})
export class AppComponent{

  products$: Observable<Product[]>;                                   4

  constructor(private httpClient: HttpClient) {                       5
     this.products$ = this.httpClient
                         .get<Product[]>('/data/products.json');      6
   }
}

  • 1 Declares a product type
  • 2 Iterates through the observable products and autosubscribes to them with the async pipe
  • 3 Renders the product title and the price formatted as currency
  • 4 Declares a typed observable for products
  • 5 Injects the HttpClient service
  • 6 Makes an HTTP GET request specifying the type of the expected data
Note

In this app, you don’t use the lifecycle hook ngOnInit() for fetching data. That’s not a crime, because this code doesn’t use any component properties that may not have been initialized during component construction. This data fetch will be executed asynchronously after the constructor when the async pipe subscribes to the products$ observable.

To see this app in action, run npm install in the client directory, and then run the following command:

ng serve --app readfile -o

It wasn’t too difficult, was it? Open Chrome Dev tools, and you’ll see the HTTP request and response and their headers, as shown in figure 12.2.

Figure 12.2. Monitoring HTTP request and response

This app illustrates how to make an HTTP GET request that has no parameters and uses default HTTP request headers. If you want to add additional headers and query parameters, use an overloaded version of the get() method that offers an extra parameter where you can specify additional options. The following listing shows how to request data from the same products.json file, passing additional headers and query parameters, using the classes HttpHeaders and HttpParams.

Listing 12.6. Adding HTTP headers and query parameters to the GET request
constructor(private httpClient: HttpClient) {
    let httpHeaders = new HttpHeaders()                                    1
       .set('Content-Type', 'application/json')
       .set('Authorization', 'Basic QWxhZGRpb');

    let httpParams = new HttpParams()                                      2
       .set('title', "First");

    this.products$ = this.httpClient.get<Product[]>('/data/products.json',
      {                                                                    3
       headers: httpHeaders,
      params: httpParams
    });

  • 1 Creates the HttpHeaders object with two additional headers
  • 2 Creates the object with one query parameter (it can be any object literal)
  • 3 Passes the headers and query parameters as a second argument of get()

Since you simply read a file, passing query parameters doesn’t make much sense, but if you needed to make a similar request to a server’s endpoint that knows how to search products by title, the code would look the same. Using the chainable set() method, you can add as many headers or query parameters as needed.

Running listing 12.7 renders the same data from products.json, but the URL of the request and HTTP headers will look different. Figure 12.3 uses arrows to highlight the differences compared to figure 12.2.

Figure 12.3. Monitoring the modified HTTP request

You may be wondering how to send data (for example, using HTTP POST) to the server. To write such an app, you need a server that can accept your data. In section 12.4.2, you’ll create an app that uses HttpClient.post(), but first let’s create a web server using the Node.js and Express.js frameworks.

12.3. Creating a web server with Node, Express, and TypeScript

Angular can communicate with web servers running on any platform, but we decided to create and use a Node.js server in this book for the following reasons:

  • There’s no need to learn a new programming language to understand the code.
  • Node allows you to create standalone applications (such as servers).
  • Using Node lets you continue writing code in TypeScript, so we don’t have to explain how to create a web server in Java, .NET, or any other language.

You’ll start with writing a basic web server using Node and Express frameworks. Then, you’ll write another web server that can serve JSON data using the HTTP protocol. After this server’s ready, you’ll create an Angular client that can consume its data.

Note

Source code for this chapter can be found at https://github.com/Farata/angulartypescript and www.manning.com/books/angular-development-with-typescript-second-edition.

12.3.1. Creating a simple web server

In this section, you’ll create a web server using Node and Express (http://expressjs.com) frameworks and TypeScript. The code that comes with this chapter has a directory called server, containing a separate project with its own package .json file, which doesn’t include any Angular dependencies. The sections for dependencies and devDependencies of this file look like the following listing.

Listing 12.7. The server’s dependencies in package.json
"dependencies": {
    "express": "^4.16.2",            1
     "body-parser": "^1.18.2"        2
   },
  "devDependencies": {
    "@types/express": "^4.0.39",     3
     "@types/node": "^6.0.57",       4
     "typescript": "^2.6.2"          5
   }

  • 1 Express.js framework
  • 2 Request body parser for Express.js
  • 3 Type definition files for Express.js
  • 4 Type definition files for Node.js
  • 5 Local version of the TypeScript compiler

You can read about type definition files in section B.12 in appendix B. You’ll use the body-parser package for extracting the data from a request object in section 12.4.

Note

You install the local version of the TypeScript compiler just in case you need to keep a different version of the tsc compiler installed globally. Also, you shouldn’t expect that a continuous integration server has a global tsc executable. To use the local tsc version, you can add a custom npm script command in the scripts section of package.json ("tsc": "tsc") and start the compiler by running the npm run tsc command.

Because you’ll write the server code in TypeScript, it needs to be transpiled, so the following listing adds the tsconfig.json file with the compiler’s options for tsc.

Listing 12.8. tsconfig.json for the server
{
  "compilerOptions": {
    "module": "commonjs",        1
     "outDir": "build",          2
     "target": "es6"             3
   },
  "exclude": [
    "node_modules"               4
   ]
}

  • 1 Transpiles modules according to the CommonJS spec
  • 2 Saves .js files into the build directory
  • 3 Transpiles into .js files using ES6 syntax
  • 4 Doesn’t transpile the code located in the node_modules directory

By specifying the CommonJS syntax for modules, you ensure that tsc transpiles statements like import * as express from "express"; into const express = require ("express");, as required by Node.

The following listing shows the code of a simple web server from the file my-express-server.ts. This server implements the server-side routing for HTTP GET requests by mapping three paths—/, /products, and /reviews—to the corresponding callback functions.

Listing 12.9. my-express-server.ts
import * as express from "express";
const app = express();                                                      1

app.get('/', (req, res) => res.send('Hello from Express'));                 2

app.get('/products', (req, res) => res.send('Got a request for products')); 3

app.get('/reviews', (req, res) => res.send('Got a request for reviews'));   4

const server = app.listen(8000, "localhost", () => {                        5

   console.log(`Listening on localhost:8000`);
});

  • 1 Instantiates Express.js
  • 2 Matches GET requests to the root route
  • 3 Matches GET requests to the /products route
  • 4 Matches GET requests to the /reviews route
  • 5 Starts listening on localhost:8000 and executes the code from the fat-arrow function

Run npm install; transpile all code samples, including my-express-server.ts, by running tsc; and start this server by running the following command:

node build/my-express-server
Note

If you don’t have the TypeScript compiler installed globally, you can either run its local version, ./node_modules/typescript/bin/tsc, or add the line "tsc: "tsc" to the scripts section of package.json, and run it like this: npm run tsc.

You’ll see the message “Listening on localhost:8000” on the console, and now you can request either products or reviews, depending on which URL you enter in the browser, as shown in figure 12.4.

Figure 12.4. Server-side routing with Express

Note

To debug Node applications, refer to the documentation of your IDE. If you want to debug the TypeScript code, don’t forget to set the option "sourceMap": true in the tsconfig.json file of your Node project.

This server responds with simple text messages, but how do you create a server that can respond with data in JSON format?

12.3.2. Serving JSON

To send JavaScript objects (such as products) to the browser in JSON format, you’ll use the Express function json() on the response object. Your REST server is located in the rest-server.ts file, and it can serve either all products or a specific one (by ID). In this server, you’ll create three endpoints: / for the root path, /api/products for all products, and /api/products/:id for the paths that include product IDs. The products array will contain three hardcoded objects of type Product, which will be turned into JSON format by invoking res.json(), offered by the Express framework.

Listing 12.10. rest-server.ts
import * as express from "express";
const app = express();

interface Product {                                                        1
     id: number,
    title: string,
    price: number
}

const products: Product[] = [                                              2
    {id: 0, title: "First Product", price: 24.99},
    {id: 1, title: "Second Product", price: 64.99},
    {id: 2, title: "Third Product", price: 74.99}
];

function getProducts(): Product[] {                                        3
     return products;
}

app.get('/', (req, res) => {                                               4
     res.send('The URL for products is http://localhost:8000/api/products');
});

app.get('/api/products', (req, res) => {                                   5
     res.json(getProducts());                                              6
});

function getProductById(productId: number): Product {                      7
     return products.find(p => p.id === productId);
}

app.get('/api/products/:id', (req, res) => {                               8
     res.json(getProductById(parseInt(req.params.id)));                    9
 });

const server = app.listen(8000, "localhost", () => {
    console.log(`Listening on localhost:8000`);
});

  • 1 Defines the Product type
  • 2 Creates an array of three JavaScript objects with products data
  • 3 Returns all products
  • 4 Returns the text prompt as a response to the base URL GET request
  • 5 When the GET request contains /api/products in the URL, invokes getProducts()
  • 6 Converts products to JSON and returns them to the browser
  • 7 Returns the product by ID. Here, you use the array’s find() method.
  • 8 The GET request came with a parameter. Its values are stored in the params property of the request object.
  • 9 Converts the product ID from a string to an integer, invokes getProductById(), and sends the JSON back

Stop the my-express-server from the previous section if it’s running (Ctrl-C), and start the rest-server with the following command:

node build/rest-server

Enter http://localhost:8000/api/products in the browser, and you should see the data in JSON format, as shown in figure 12.5.

Figure 12.5. The server’s response to http://localhost:8000/api/products

Figure 12.6 shows the browser window after you enter the URL http://localhost:8000/api/products/1. This time, the server returns only data about the product that has an id with the value of 1.

Figure 12.6. The server’s response to http://localhost:8000/api/products/1

Your REST server is ready. Now let’s see how to initiate HTTP GET requests and handle responses in Angular applications.

12.4. Bringing Angular and Node together

In the preceding section, you created the rest-server.ts file, which responds to HTTP GET requests with product details regardless of whether the client was written using a framework or the user simply entered the URL in a browser. In this section, you’ll write an Angular client that will issue HTTP GET requests and treat the product data as an Observable data stream returned by your server.

Note

Just a reminder: the Angular app and the Node server are two separate projects. The server code is located in the directory called server, and the Angular app is located in a separate project in the client directory.

12.4.1. Static assets on the server

A typical web app deployed on the server includes static assets (for example, HTML, images, CSS, and JavaScript code) that have to be loaded by the browser when the user enters the URL of the app. From the server’s perspective, the Angular portion of a web app is considered static assets. The Express framework allows you to specify the directory where the static assets are located.

Let’s create a new server: rest-server-angular.ts. In the rest-server.ts file from the previous section, you didn’t specify the directory with static assets, because no client app was deployed on the server. In the new server, you add the lines shown in the following listing.

Listing 12.11. Specifying the directory with static resources
import * as path from "path";                                     1

app.use('/', express.static(path.join(__dirname, 'public')));     2

  • 1 Adds the Node path module for working with the directory and paths
  • 2 Assigns the public subdirectory as the location of the static resources

Unlike in rest-server.ts, you just map the base URL (/) to the public directory, and Node will send index.html from there by default. The browser loads index.html, which in turn loads the rest of the bundles defined in the <script> tags.

Note

The original index.html file generated by Angular CLI doesn’t contain <script> tags, but when you run the ng build or ng serve commands, they create a new version of index.html that includes the <script> tags with all the bundles and other assets.

When the browser requests static assets, Node will look for them in the public subdirectory of the current one (__dirname)—the build directory from which you started this server. Here, you use Node’s path.join() API to ensure that the absolute file path is created in a cross-platform way. In the next section, we’ll introduce the Angular client and deploy its bundles in the public directory. The REST endpoints in rest-server-angular.ts remain the same as in rest-server.ts:

  • / serves index.html, which contains the code to load the Angular app.
  • /api/products serves all products.
  • /api/products/:id serves one product by its ID.

The complete code of the rest-server-angular.ts file is shown in the next listing.

Listing 12.12. rest-server-angular.ts
import * as express from "express";
import * as path from "path";                                     1

const app = express();

app.use('/', express.static(path.join(__dirname, 'public')));     2

interface Product {
    id: number,
    title: string,
    price: number
}

const products: Product[] = [
    {id: 0, title: "First Product", price: 24.99},
    {id: 1, title: "Second Product", price: 64.99},
    {id: 2, title: "Third Product", price: 74.99}
];

function getProducts(): Product[] {
    return products;
}

app.get('/api/products', (req, res) => {                         3
     res.json(getProducts());
});

function getProductById(productId: number): Product {
    return products.find(p => p.id === productId);
}

app.get('/api/products/:id', (req, res) => {                     4
     res.json(getProductById(parseInt(req.params.id)));
});

const server = app.listen(8000, "localhost", () => {             5
     console.log(`Listening on localhost:8000`);
});

  • 1 Adds the path module that provides utilities for working with file and directory paths
  • 2 For the root path, specifies the directory from which to serve static assets
  • 3 Configures the endpoint for HTTP GET requests
  • 4 Configures another endpoint for HTTP GET requests
  • 5 Starts the server

The new server is ready to serve JSON data to the Angular client, so let’s start it:

node build/rest-server-angular

Trying to make a request to this server using the base URL http://localhost:8000 will return a 404 error, because the directory with static assets doesn’t contain the index.html file: you haven’t deployed your Angular app there yet. Your next task is to create and deploy the Angular app that will consume JSON-formatted data.

12.4.2. Consuming JSON in Angular apps

Your Angular app will be located in a directory called client. In previous chapters, you were starting all Angular apps building bundles in memory with ng serve, but this time you’ll also use the ng build command to generate bundles in files. Then you’ll use npm scripts to automate the deployment of these bundles in the Node server created in section 12.4.1.

In dev mode, you’ll keep serving Angular apps using the dev server from Angular CLI that runs on port 4200. But the data will be coming from another web server, powered by Node and Express, that will run on port 8000. Figure 12.7 illustrates this two-server setup.

Figure 12.7. Two servers in dev mode

Note

Spoiler alert: We’ll run into an issue when the client app served from one server tries to directly access another one. We’ll cross that bridge when we get to it.

When the Angular HttpClient object makes a request to a URL, the response comes back as Observable, and the client’s code can handle it either by using the subscribe() method or with the async pipe introduced in section 6.5 in chapter 6. Using the async pipe is preferable, but we’ll show you both methods so you can appreciate the advantages of async.

Let’s start with an app that retrieves all products from the rest-server-angular server and renders them using an HTML unordered list (<ul>). You can find this app in the app.component.ts file located in the client/src/app/restclient directory.

Listing 12.13. restclient/app.component.ts
// import statements omitted for brevity
interface Product {                                                     1
   id: number,
  title: string,
  price: number
}

@Component({
  selector: 'app-root',
  template: `<h1>All Products</h1>
  <ul>
    <li *ngFor="let product of products">
       {{product.title}}: {{product.price | currency}}                  2
     </li>
  </ul>
  {{error}}
  `})
export class AppComponent implements OnInit {

  products: Product[] = [];
  theDataSource$: Observable<Product[]>;                                3
   productSubscription: Subscription;                                   4
   error: string;                                                       5

  constructor(private httpClient: HttpClient) {                         6
     this.theDataSource$ = this.httpClient
              .get<Product[]>('http://localhost:8000/api/products');    7
   }

  ngOnInit() {
    this.productSubscription = this.theDataSource$
      .subscribe(                                                       8
         data => this.products = data,                                  9
         (err: HttpErrorResponse) =>
          this.error = `Can't get products. Got ${err.message}`         10
       );
  }
}

  • 1 Declares the type for products
  • 2 Uses the currency pipe for rendering the price
  • 3 Declares an observable for data returned by HttpClient
  • 4 Declares the subscription property—you’ll need to unsubscribe from observable
  • 5 The HTTP requests errors (if any) are displayed here.
  • 6 Injects HttpClient
  • 7 Declares the intention to issue HTTP GET for products
  • 8 Makes an HTTP GET request for products
  • 9 Adds the received products to the array
  • 10 Sets the value of an error message to a variable for rendering on the UI
Note

You didn’t use ngOnDestroy() to explicitly unsubscribe from the observable because once HttpClient gets the response (or an error), the underlying Observable completes, so the observer is unsubscribed automatically.

You already started the server in the previous section. Now, start the client by running the following command:

ng serve --app restclient -o

No products are rendered by the browser, and the console shows a 404 error, but if you used the full URL in the AppComponent (for example, http://localhost:8000/api/products), the browser’s console would show the following error:

Failed to load http://localhost:8000/api/products:
  No 'Access-Control-Allow-
     Origin' header is present on the requested resource.
  Origin 'http://localhost:4200' is therefore not allowed access.

That’s because you violated the same-origin policy (see http://mng.bz/2tSb). This restriction is set for clients that run in a browser as a security mechanism. Say you visited and logged in to bank.com, and then opened another tab and opened badguys.com. The same-origin policy ensures that scripts from badguys.com can’t access your account at bank.com.

Your Angular app was loaded from http://localhost:4200 but tries to access the URL http://localhost:8000. Browsers aren’t allowed to do this unless the server that runs on port 8000 is configured to allow access to the clients with the origin http://localhost:4200. When your client app is deployed in the Node server, you won’t have this error, because the client app will be loaded from the server that runs on port 8000, and this client will be making data requests to the same server.

In the hands-on section in chapter 13, you’ll use the Node.js CORS package (see https://github.com/expressjs/cors) to allow requests from clients with other origins, but this may not be an option if you need to make requests to third-party servers. In dev mode, there’s a simpler solution to the same-origin restriction. You’ll use the server that runs on port 4200 as a proxy for client requests to the server that runs on port 8000. The same-origin policy doesn’t apply to server-to-server communications. In the next section, you’ll see how to configure such a proxy on the client.

12.4.3. Configuring the client proxy

In dev mode, you’d like to continue using the server that comes with Angular CLI with its hot reload features and fast rebuilding of application bundles in memory. On the other hand, you want to be able to make requests to other servers.

Under the hood, the Angular CLI dev server uses the Webpack dev server, which can serve as a proxy mediating browser communications with other servers. You just need to create a proxy-conf.json file in the root directory of the Angular project, where you’d configure a URL fragment(s) that the dev server should redirect to another server. In your case, you want to redirect any request that has the URL fragment /api to the server that runs on port 8000, as shown in the following listing.

Listing 12.14. proxy-conf.json
{
  "/api": {                                   1
     "target": "http://localhost:8000",       2
     "secure": false                          3
   }
}

  • 1 Hijacks all requests that have /api in the URL
  • 2 Redirects these requests to this URL
  • 3 The target connection doesn’t need SSL certificates.
Note

Using a proxy file allows you to easily switch between local and remote servers. Just change the value of the target property to have your local app retrieve data from a remote server. You can read more about Angular CLI proxying support at http://mng.bz/fLgf.

You need to make a small change in the app from the preceding section. You should replace the full URL of the backend server (http://localhost:8000/api/products) with the path of the endpoint (/api/products). The code that makes a request for products will look as if you try to access the /api/products endpoint on the Angular CLI dev server where the app was downloaded from:

this.theDataSource = this.httpClient
              .get<Product[]>('/api/products');

But the dev server will recognize the /api fragment in the URL and will redirect this request to another server that runs on port 8000, as shown in figure 12.8 (compare it to figure 12.7).

Figure 12.8. Two servers with a proxy

To see the modified app in action, you need to use the --proxy-config option, providing the name of the file where you configured the proxy parameters:

ng serve --app restclient --proxy-config proxy-conf.json -o
Note

If you forget to provide the name of the proxy file when configuring proxy parameters, you’ll get a 404 error, because the /api/products request won’t be redirected, and there’s no such endpoint in the server that runs on port 4200.

Open your browser to http://localhost:4200, and you’ll see the Angular app shown in figure 12.9.

Figure 12.9. Retrieving all products from the Node server via proxy

Note that data arrives from the server that runs on port 4200, which got it from the server that runs on port 8000. Figure 12.8 illustrates this data flow.

In dev mode, using Angular CLI proxying allows you to kill two birds with one stone: have the hot reload of your app on any code change and access data from another server without the need to deploy the app there.

Now let’s see how to replace the explicit subscription for products with the async pipe.

12.4.4. Subscribing to observables with the async pipe

We introduced AsyncPipe (or async, when used in templates) in section 6.5 of chapter 6. async can receive an Observable as input, autosubscribe to it, and discard the subscription when the component gets destroyed. To see this in action, make the following changes in listing 12.13:

  • Change the type of the products variable from Array to Observable.
  • Remove the declaration of the variable theDataSource$.
  • Remove the invocation of subscribe() in the code. You’ll assign the Observable returned by the get() method to products.
  • Add the async pipe to the *ngFor loop in the template.

The following listing implements these changes (see the file restclient/app.component.asyncpipe.ts).

Listing 12.15. app.component.asyncpipe.ts
import { HttpClient} from '@angular/common/http';
import {Observable, EMPTY} from 'rxjs';
import {catchError} from 'rxjs/operators';
import {Component} from "@angular/core";

interface Product {
  id: number,
  title: string,
  price: number
}

@Component({
  selector: 'app-root',
  template: `<h1>All Products</h1>
  <ul>
    <li *ngFor="let product of products$ | async">                              1
       {{product.title }} {{product.price | currency}}
    </li>
  </ul>
  {{error}}
  `})
export class AppComponentAsync{

  products$: Observable<Product[]>;
  error: string;
  constructor(private httpClient: HttpClient) {
    this.products$ = this.httpClient.get<Product[]>('/api/products')            2
       .pipe(
        catchError( err => {                                                    3
           this.error = `Can't get products. Got ${err.status} from ${err.url}`;4
           return EMPTY;
          });                                                                   5
  }
}

  • 1 The async pipe subscribes and unwraps products from observable products$.
  • 2 Initializes the observable with HttpClient.get()
  • 3 Intercepts an error before it reaches the async pipe
  • 4 Handles the error, if any
  • 5 Returns an empty observable so the subscriber won’t get destroyed

Running this application will produce the same output shown in figure 12.9.

So far, you’ve been injecting HttpClient instances directly into components, but more often you inject HttpClient into a service. Let’s see how to do this.

12.4.5. Injecting HttpClient into a service

Angular offers an easy way for separating the business logic implementation from rendering the UI. Business logic should be implemented in services, and the UI in components, and you usually implement all HTTP communications in one or more services that are injected into components. For example, your ngAuction app that comes with chapter 11 has the ProductService class with the injected HttpClient service. You inject a service into another service.

ProductService reads the products.json file using HttpClient, but it could get the product data from a remote server the same way you did in the previous section. ProductService is injected into components of ngAuction. Check the source code of ProductService and CategoriesComponent in ngAuction that comes with chapter 11, and you’ll recognize the pattern shown in figure 12.10.

Figure 12.10. Injecting into a service and a component

The following listing from ngAuction’s ProductService is an example of encapsulating business logic and HTTP communications inside a service.

Listing 12.16. A fragment from ngAuction’s ProductService
@Injectable()
export class ProductService {
  constructor(private http: HttpClient) {}                                 1

  getAll(): Observable<Product[]> {
    return this.http.get<Product[]>('/data/products.json');                2
   }

  getById(productId: number): Observable<Product> {
    return this.http.get<Product[]>('/data/products.json')                 2
       .pipe(
        map(products => products.find(p => p.id === productId))
      );
  }

  getByCategory(category: string): Observable<Product[]> {
    return this.http.get<Product[]>('/data/products.json').pipe(           2
       map(products => products.filter(p => p.categories.includes(category)))
     );
  }

  getDistinctCategories(): Observable<string[]> {
   return this.http.get<Product[]>('/data/products.json')                  2
     .pipe(
      map(this.reduceCategories),
      map(categories => Array.from(new Set(categories))),
    );
  }
  // Other code is omitted for brevity
}

  • 1 Injects HttpClient into ProductService
  • 2 Invokes HttpClient.get()
Note

In the preceding listing 12.19, you use RxJS pipeable operators inside the pipe() method (see section D.4.1 in appendix D).

The next listing from CategoriesComponent is an example of using the preceding service in the component.

Listing 12.17. A fragment from the ngAuction’s CategoriesComponent
@Component({
  selector: 'nga-categories',
  styleUrls: [ './categories.component.scss' ],
  templateUrl: './categories.component.html'
})
export class CategoriesComponent {
  readonly categoriesNames$: Observable<string[]>;
  readonly products$: Observable<Product[]>;

  constructor(
    private productService: ProductService,                              1
     private route: ActivatedRoute
  ) {
    this.categoriesNames$ = this.productService.getDistinctCategories()  2
         .pipe(map(categories => ['all', ...categories]));

    this.products$ = this.route.params.pipe(
      switchMap(({ category }) => this.getCategory(category)));
  }

  private getCategory(category: string): Observable<Product[]> {
    return category.toLowerCase() === 'all'
      ? this.productService.getAll()                                     2
       : this.productService.getByCategory(category.toLowerCase());      2
   }
}

  • 1 Injects ProductService
  • 2 Uses ProductService

The provider for ProductService is declared on the app level in the @NgModule() decorator of the root module of ngAuction. In the hands-on section in chapter 13, you’ll split ngAuction into two projects, client and server, and the web server will be written using Node and Express frameworks. How can an Angular app (bundles and assets) be deployed in a web server?

12.4.6. Deploying Angular apps on the server with npm scripts

The process of deploying the code of a web client in a server should be automated. At the very minimum, deploying an Angular app includes running several commands for building bundles and replacing previously deployed code (index.html, JavaScript bundles, and other assets) with new code. The deployment may also include running test scripts and other steps.

JavaScript developers use various tools for automating running deployment tasks such as Grunt, gulp, npm scripts, and others. In this section we’ll show you how to use npm scripts for deployment. We like using npm scripts, because they’re simple to use and offer an easy way to automate running command sequences in a predefined order. Besides, you already have npm installed, so there’s no need to install additional software for automating your deployment workflow.

To illustrate the deployment process, you’ll use the rest-server-angular server from section 12.3.1, where you’ll deploy the Angular app from section 12.3.4. After deployment, you won’t need to configure the proxy anymore, because both the server and the client code will be deployed at the same server running http://localhost:8000. After entering this URL in the browser, the user will see the product data, as shown earlier in figure 12.9.

npm allows you to add the scripts property in package.json, where you can define aliases for terminal commands. For example, instead of typing the long command ng serve --app restclient --proxy-config proxy-conf.json, you can define a start command in the scripts section of package.json as follows:

"scripts": {
  "start": "ng serve --app restclient --proxy-config proxy-conf.json"
}

Now, instead of typing that long command, you’ll just enter npm start in the console. npm supports more than a dozen script commands right out of the box (see the npm-scripts documentation for details, https://docs.npmjs.com/misc/scripts). You can also add new custom commands specific to your development and deployment workflow.

Some of these scripts need to be run manually (such as npm start), and some are invoked automatically if they have the post and pre prefixes (for example, post-install). If any command in the scripts section starts with the post prefix, it’ll run automatically after the corresponding command specified after this prefix.

For example, if you define the command "postinstall": "myCustomInstall.js", each time you run npm install, the script myCustomInstall.js will automatically run right after. Similarly, if a command has a pre prefix, such a command will run before the command named after this prefix.

If you define custom commands that aren’t known by npm scripts, you’ll need to use an additional option: run. Say you defined a custom command startDev like this:

"scripts": {
  "startDev": "ng serve --app restclient --proxy-config proxy-conf.json"
}

To run that command, you need to enter the following in your terminal window: npm run startDev. To automate running some of your custom commands, use the same prefixes: post and pre.

Let’s see how to create a sequence of runnable commands for deploying an Angular app on the Node server. Open package.json from the client directory, and you’ll find four custom commands there: build, postbuild, predeploy, and deploy. The following listing shows what will happen if you run a single command: npm run build.

Listing 12.18. A fragment from client/package.json
"scripts": {
  "build": "ng build --prod --app restclient",                                   1
   "postbuild": "npm run deploy",                                                2
   "predeploy": "rimraf ../server/build/public && mkdirp ../server/build/public",3
   "deploy": "copyfiles -f dist/** ../server/build/public"                       4
 }

  • 1 The command ng build will create a production build of the restclient app in the default directory dist.
  • 2 Since there’s a postbuild command, it starts automatically and will try to run the deploy command.
  • 3 Since there’s also a predeploy command there, it’ll run after the postbuild and before deploy.
  • 4 Finally, the deploy command is executed.

We’ll explain what the commands predeploy and deploy do in a minute, but our main message here is that starting a single command resulted in running four commands in the specified order. Creating a sequence of deployment commands is easy.

Tip

If you build the bundles with AOT compilation and use only standard Angular decorators (no custom ones), you can further optimize the size of the JavaScript in your app by commenting out the line import 'core-js/es7/ reflect'; in the polyfills.ts file. This will reduce the size of the generated polyfill bundle.

Typically, the deployment process removes the directory with previously deployed files, creates a new empty directory, and copies the new files into this directory. In your deployment scripts, you use three npm packages that know how to do these operations, regardless of the platform you use (Windows, Unix, or macOS):

  • rimrafRemoves the specified directory and its subdirectories
  • mkdirpCreates a new directory
  • copyfilesCopies files from source to destination

Check the devDependencies section in package.json, and you’ll see rimraf, mkdirp, and copyfiles there.

Tip

Currently, Angular CLI uses Webpack to build bundles. Angular CLI 7 will come with new build tools. In particular, it’ll include Closure Compiler, which produces smaller bundles.

The code that comes with this chapter is located in two sibling directories: client and server. Your predeploy command removes the content of the server/build/public directory (this is where you’ll deploy the Angular app) and then creates a new empty public directory. The && sign allows you to define commands that run more than one script.

The deploy command copies the content of the client/dist directory (the app’s bundles and assets) into server/build/public.

In the real world, you may need to deploy an Angular app on a remote server, so using the package copyfiles won’t work. Consider using an SCP utility (see https://en.wikipedia.org/wiki/Secure_copy) that performs secure file transfer from a local computer to a remote one.

If you can manually run a utility from the terminal window, you can run it using npm scripts as well. In chapter 14, you’ll learn how to write test scripts. Including a test runner into your build process could be as simple as adding && ng test to your predeploy command. If you find some useful gulp plugins, create the npm script for it, for example, "myGulpCommand" : "gulp SomeUsefulTask".

To see that your deployment scripts work, perform the following steps:

1.  Start the server by running the following command in the server directory:

node build/rest-server-angular

2.  In the client directory, run the build and deployment scripts:

npm run build

Check the server/build/public directory—the client’s bundles should be there.

3.  Open your browser to http://localhost:8000, and your Angular app will be loaded from your Node server showing three products, as shown in figure 12.9.

We’ve described the entire process of creating and running a web server, as well as creating and running Angular apps in dev mode and deploying in the server. Your Angular app was using the HttpClient service to issue HTTP GET requests to retrieve data from the server. Now let’s see how to issue HTTP POST requests to post data to the server.

12.5. Posting data to the server

HTTP POST requests are used for sending new data to the server. With HttpClient, making POST requests is similar to making GET requests. Invoking the HttpClient.post() method declares your intention to post data to the specified URL, but the request is made when you invoke subscribe().

Note

For updating existing data on the server, use HttpClient.put(); and for deleting data, use HttpClient.delete().

12.5.1. Creating a server for handling post requests

You need a web server with an endpoint that knows how to handle POST requests issued by the client. The code that comes with this chapter includes a server with an /api/product endpoint for adding new products, located in the rest-server-angular-post.ts file. Because your goal isn’t to have a fully functional server for adding and saving products, the /api/product endpoint will simply log the posted data on the console and send a confirmation message to the client.

The posted data will arrive in the request body, and you need to be able to parse it to extract the data. The npm package body-parser knows how to do this in Express servers. If you open package.json in the server directory, you’ll find body-parser in the dependencies section. The entire code of your server is shown in the following listing.

Listing 12.19. rest-server-angular-post.ts
import * as express from "express";
import * as path from "path";
import * as bodyParser from "body-parser";                           1

const app = express();

app.use('/', express.static(path.join(__dirname, 'public')));

app.use(bodyParser.json());                                          2

app.post("/api/product", (req, res) => {                             3

  console.log(`Received new product
               ${req.body.title} ${req.body.price}`);                4

  res.json({'message':`Server responded: added ${req.body.title}`}); 5
 });

const server = app.listen(8000, "localhost", () => {
  const {address, port} = server.address();
  console.log(`Listening on ${address}: ${port}`);
});

  • 1 Adds the body-parser package
  • 2 Creates the parser to turn the payload of req.body into JSON
  • 3 Creates an endpoint for handling POST requests
  • 4 Logs the payload of the POST request
  • 5 Sends the confirmation message to the client

Your server expects the payload in a JSON format, and it’ll send the response back as a JSON object with one property: message. Start this server by running the following command in the server directory (don’t forget to run tsc to compile it):

node build/rest-server-angular-post

Testing a RESTful API

When you create web servers with REST endpoints, you should test them to ensure that the endpoints work properly even before you start writing any client code. Your IDE may offer such a tool. For example, the WebStorm IDE has a menu item, Test RESTful Web Service, under Tools. After entering all the data to test your server, this tool looks like the following figure.

Using WebStorm’s Test RESTful client

The server response

Press the green play button, and you’ll see the response of your /api/product endpoint under the Response tab, as shown in the next figure.

The server response

If your IDE doesn’t offer such a testing tool, use the Chrome extension called Advanced REST Client (https://install.advancedrestclient.com/#/install) or a tool called Postman (www.getpostman.com).

Now that you’ve created, started, and tested the web server, let’s write the Angular client that will post new products to this server.

12.5.2. Creating a client for making post requests

Your Angular app will render a simple form where the user can enter the product title and price, as shown in figure 12.11.

Figure 12.11. UI for adding new products

After filling out the form and clicking the Add Product button, the server will respond with the confirmation message shown under the button.

In this app, you’ll use the template-driven Forms API, and your form will require the user to enter the new product’s title and price. On the button click, you’ll invoke the method HttpClient.post(), followed by subscribe().

The code of this Angular app is located in the client directory under the restclientpost subdirectory. It’s shown in the following listing.

Listing 12.20. app.component.ts
import {Component} from "@angular/core";
import {HttpClient, HttpErrorResponse} from "@angular/common/http";

@Component({
  selector: 'app-root',
  template: `<h1>Add new product</h1>
     <form #f="ngForm" (ngSubmit) = "addProduct(f.value)" >
       Title: <input id="productTitle" name="title" ngModel>
       <br>
       Price: <input id="productPrice" name="price" ngModel>
       <br>
       <button type="submit">Add product</button>
     </form>
     {{response}}
  `})
export class AppComponent {

  response: string;

  constructor(private httpClient: HttpClient) {}

  addProduct(formValue) {

    this.response='';

    this.httpClient.post<string>("/api/product",                1
                                  formValue)                    2
       .subscribe(                                              3
         data =>  this.response = data['message'],              4
         (err: HttpErrorResponse) =>                            5
             this.response = `Can't add product. Error code:
              ${err.message} ${err.error.message}`
      );
  }
}

  • 1 Declares the intention to make a POST request
  • 2 Provides the POST payload
  • 3 Makes the HTTP POST request
  • 4 Gets the server’s response
  • 5 Handles errors

When the user clicks Add Product, the app makes a POST request and subscribes to the server response. formValue contains a JavaScript object with the data entered in the form, and HttpClient automatically turns it into a JSON object. If the data was posted successfully, the server returns a JSON object with the message property, which is rendered using data binding.

If the server responds with an error, you display it in the UI. Note that you use err.message and err.error.message to extract the error description. The second property may contain additional error details. Modify the code of the server to return a string instead of JSON, and the UI will show a detailed error message.

To see this app in action, run the following command in the project client:

ng serve --app restclientpost --proxy-config proxy-conf.json -o

You now know how to send HTTP requests and handle responses, and there could be lots of them in your app. Is there a way to intercept all of them to provide some additional processing, like showing/hiding the progress bar, or logging requests?

12.6. HTTP interceptors

Angular allows you to create HTTP interceptors for pre- and post-processing of all HTTP requests and responses of your app. They can be useful for implementing such cross-cutting concerns as logging, global error handling, authentication, and others. We’d like to stress that the interceptors work before the request goes out or before a response is rendered on the UI. This gives you a chance to implement the fallback scenarios for certain errors or prevent attempts of unauthorized access.

To create an interceptor, you need to write a service that implements the HttpInterceptor interface, which requires you to implement one method: intercept(). Angular will provide two arguments to this callback: HttpRequest and HttpHandler. The first one contains the request object being intercepted, which you can clone and modify. The second argument is used to forward the modified request to the backend or another interceptor in the chain (if any) by invoking the handle() method.

Note

The HttpRequest and HttpResponse objects are immutable, and the word modify means creating and passing through the new instances of these objects.

The interceptor service shown in the following listing doesn’t modify the outgoing HttpRequest but simply prints its content on the console and passes it through as is.

Listing 12.21. A simple interceptor
@Injectable()
export class MyFirstInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Clone and modify your HTTPRequest using req.clone()
    // or perform other actions here

    console.log("I've intercepted your HTTP request! ${JSON.stringify(req)}`);

    return next.handle(req);
  }
}

In listing 12.21, you forward the HttpRequest, but you could modify its headers or parameters and return the modified request. The next.handle() method returns an observable when the request is complete, and if you want to modify the HTTP response as well, apply additional RxJS operators on the stream returned by next.handle().

The intercept() method receives the HttpRequest object and returns, not the HttpResponse object, but the observable of HttpEvent, because Angular implements HttpResponse as a stream of HttpEvent values.

Both HttpRequest and HttpResponse are immutable, and if you want to modify their properties, you need to clone them first, as in the following listing.

Listing 12.22. Modifying HTTPRequest
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any
     >> {
    const modifiedRequest = req.clone({
      setHeaders: { ('Authorization', 'Basic QWxhZGRpb') }
    });
    return next.handle(modifiedRequest);
  }

Because an interceptor is an injectable service, don’t forget to declare its provider for the HTTP_INTERCEPTORS token in the @NgModule() decorator:

providers: [{provide: HTTP_INTERCEPTORS,
             useClass: MyFirstInterceptor, multi: true}]

The multi: true option tells you that HTTP_INTERCEPTORS is a multiprovider token—an array of services can represent the same token. You can register more than one interceptor, and Angular will inject all of them:

providers: [{provide: HTTP_INTERCEPTORS,
               useClass: MyFirstInterceptor, multi: true},
            {provide: HTTP_INTERCEPTORS,
               useClass: MySecondInterceptor, multi: true}]
Note

If you have more than one interceptor, they’ll be invoked in the order they’re defined.

To illustrate how interceptors work, let’s create an app with an HttpInterceptor that will intercept and log all errors returned by the server. For the client, you’ll reuse the app from section 12.5.2 shown in figure 12.11, adding the logging service and the interceptor to log errors on the console.

You’ll slightly modify the server from the previous section to randomly generate errors. You can find the complete code of the server in the rest-server-angular-post-errors.ts file. Now, instead of just responding with success messages, it’ll randomly return an error, as shown in the following listing.

Listing 12.23. Emulating server errors
if (Math.random() < 0.5) {                                                1
     res.status(500);
    res.send({'message': `Server responded: error adding product
                          ${req.body.title}`});
} else {                                                                  2
     res.send({'message': `Server responded: added ${req.body.title}`);
}

  • 1 Returns an HTTP response with the status 500
  • 2 Returns a successful HTTP response

Start this server as follows:

node build/rest-server-angular-post-errors

Your Angular app is located in the interceptor directory and includes a logging service implemented as two classes: LoggingService and ConsoleLoggingService. LoggingService is an abstract class that declares one method, log().

Listing 12.24. logging.service.ts
@Injectable()
export abstract class LoggingService {

  abstract log(message: string): void;
}

Because this class is abstract, it can’t be instantiated, and you’ll create the class ConsoleLoggingService shown in the following listing.

Listing 12.25. console.logging.service.ts
@Injectable()
export class ConsoleLoggingService implements LoggingService{

   log(message:string): void {
        console.log(message);
   }
}

You may be wondering why you create the abstract class for such a simple logging service. It’s because in real-world apps, you may want to introduce logging, not only on the browser’s console, but also on the server. Having an abstract class would allow you to use it as a token for declaring a provider:

providers: [{provide: LoggingService, useClass: ConsoleLoggingService}]

Later on, you can create a class called ServerLoggingService that implements LoggingService, and to switch from console to server logging, you’ll need to change the provider without having to modify components that use it:

providers: [{provide: LoggingService, useClass: ServerLoggingService}]

If your interceptor receives an error, you’ll do the following:

1.  Log it on the console.

2.  Replace the HttpErrorResponse with a new instance of HttpResponse that will contain the error message.

3.  Return the new HttpResponse so the client can show it to the user.

The interceptor class will use the catchError operator on the observable returned by HttpHandler.next(), where you’ll implement these steps. Your interceptor is implemented in the logging.interceptor.service.ts file.

Listing 12.26. logging.interceptor.service.ts
import {Injectable} from "@angular/core";
import {HttpErrorResponse, HttpEvent, HttpHandler,
      HttpInterceptor, HttpRequest, HttpResponse} from "@angular/common/http";
import {Observable, of} from "rxjs";
import {catchError} from 'rxjs/operators';
import {LoggingService} from "./logging.service";

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {

  constructor(private loggingService: LoggingService) {}                        1

  intercept(req: HttpRequest<any>, next: HttpHandler):
Observable<HttpEvent<any>> {
    return next.handle(req)                                                     2
       .pipe(
        catchError((err: HttpErrorResponse) =>                                  3
           this.loggingService.log(`Logging Interceptor: ${err.error.message}`);4
           return of(new HttpResponse(                                          5
               {body:{message: err.error.message}}));                           6
          })
      );
  }
}

  • 1 Injects the console logging service
  • 2 Forwards requests to the server and responses to the client
  • 3 Catches the response errors returned by the server
  • 4 Logs the error message on the console
  • 5 Replaces HttpErrorResponse with HttpResponse
  • 6 The new HttpResponse will contain the error message.

The code for the application component has no references to the interceptor class, as you’ll see in the following listing. It’ll be always receiving HttpResponse objects that contain either a message that the server successfully added a new product, or an error message.

Listing 12.27. app.component.ts
import {Component} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs";
import {map} from "rxjs/operators";

@Component({
  selector: 'app-root',
  template: `<h1>Add new product</h1>
  <form #f="ngForm" (ngSubmit) = "addProduct(f.value)" >
    Title: <input id="productTitle" name="title" ngModel>
    <br>
    Price: <input id="productPrice" name="price" ngModel>
    <br>
    <button type="submit">Add product</button>
  </form>
  {{response$ | async}}                                                    1
   `})
export class AppComponent {

  response$: Observable<string>;                                           2

  constructor(private httpClient: HttpClient) {}

  addProduct(formValue){
    this.response$=this.httpClient.post<{message: string}>("/api/product", 3
       formValue)
      .pipe(
        map (data=> data.message)                                          4
       );
  }
}

  • 1 Renders any messages received from the server (including errors)
  • 2 This observable is for the interceptor’s responses.
  • 3 Expects the server’s responses to HTTP POST as {message: string}
  • 4 Extracts the text of the message property

When you compare this app with the one from the previous section, note that you don’t handle errors in the component, which renders the messages to the UI. Now the LoggingInterceptor will handle all HTTP errors.

To see this app in action, run the following command and monitor the browser console for logging messages.

ng serve --app interceptor --proxy-config proxy-conf.json -o

This app should give you an idea of how to implement a cross-cutting concern like a global error-logging service for all HTTP responses without the need to modify any application components or services that use the HttpClient service.

An HTTP request runs asynchronously and can generate a number of progress events that you might want to intercept and handle. Let’s look at how you’d do that.

12.7. Progress events

Sometimes uploading or downloading certain assets (like large data files or images) takes time, and you should keep the user informed about the progress. HttpClient offers progress events that contain information like total size of the asset, current number of bytes that are already uploaded or downloaded, and more.

To enable progress events tracking, make your requests using the HttpRequest object with the option {reportProgress: true}. For example, you can make an HTTP GET request that reads the my_large_file.json file.

Listing 12.28. Making a GET request with events tracking
const req = new HttpRequest('GET',                      1
                         './my_large_file.json',        2
                         { reportProgress: true });     3
 httpClient.request(req).subscribe(                     4
        // Handle progress events here);

  • 1 Declares an intention to make a GET request
  • 2 Specifies the file to read
  • 3 Enables progress event
  • 4 Makes a request

In the subscribe() method, check whether the emitted value is an event of the type you’re interested in, for example, HttpEventType.DownloadProgress or HttpEventType.UploadProgress. These events have the loaded property for the current number of transferred bytes and the total property, which knows the total size of the transfer.

The next app shows how to handle a progress event for calculating and showing the percentage of the file download. This app comes with a large 48 MB JSON file. The content of the file is irrelevant in this case. Figure 12.12 shows the app when the download of the file is complete. The percentage on the left is changing as this file is being loaded by HttpClient. This app also reports the progress on the browser’s console.

Figure 12.12. Reporting progress while reading a file

This app is located in the progressevents directory, and the content of app.component .ts is shown in the next listing.

Listing 12.29. app.component.ts
import {HttpClient, HttpEventType, HttpRequest} from '@angular/common/http';
import {Component} from "@angular/core";

@Component({
  selector: 'app-root',
  template: `<h1>Reading a file: {{percentDone}}% done</h1>                1
   `})
export class AppComponent{

  mydata: any;
  percentDone: number;

  constructor(private httpClient: HttpClient) {

    const req = new HttpRequest('GET',                                     2
                                './data/48MB_DATA.json',                   3
                                {reportProgress: true});                   4

    httpClient.request(req)
    .subscribe(data => {
      if (data.type === HttpEventType.DownloadProgress) {                  5
         this.percentDone = Math.round(100 * data.loaded / data.total);))  6
         console.log(`Read ${this.percentDone}% of ${data.total} bytes`);
      } else {
        this.mydata = data                                                 7
       }
    });
  }
}

  • 1 Renders the current percentage
  • 2 Declares an intention to make a GET request
  • 3 Specifies the file to read
  • 4 Enables the progress event tracking
  • 5 Checks the type of the progress event
  • 6 Calculates the current percentage
  • 7 Emitted value is not a progress event

To see this app in action, run the following command:

ng serve --app progressevents -o

This app concludes our coverage of communicating with web servers using HTTP. In the next chapter, you’ll see how an Angular client can communicate with web servers using WebSockets.

Summary

  • Angular comes with the HttpClient service, which supports HTTP communications with web servers.
  • Public methods of HttpClient return an Observable object, and only when the client subscribes to it is the request to the server made.
  • An Angular client can communicate with web servers implemented in different technologies.
  • You can intercept and replace HTTP requests and responses with modified ones to implement cross-cutting concerns.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.14.63