-
Notifications
You must be signed in to change notification settings - Fork 2
Retrieving Data Using HTTP
The data for our application is on a server somewhere, in the cloud, at the office, under our desk. How do we get that data into our view? Welcome back to Angular: Getting Started, from Pluralsight. My name is Deborah Kurata, and in this chapter, we learn how to use HTTP with observables to retrieve data. Most Angular applications obtain data using HTTP. The application issues an HTTP GET request to a web service. That web service retrieves the data, often using a database, and returns it to the application in an HTTP response. The application then processes that data. In this chapter, we begin with an introduction to observables and the reactive extensions. We then examine how to send an HTTP request and map the result to an array. We add some exception handling, and we look at how to subscribe to observables to get the data for our view. We finished the first cut of our product data service in the last chapter, but it still has hardcoded data. Now, we'll replace that hardcoded data with HTTP calls. Let's get started.
Data sequences can take many forms, such as a response from a back-end web service, a set of system notifications or a stream of events, such as user input. In this diagram, our stream contains three elements. The first marble comes in, then the second, and some time later, the third. Reactive extensions represent a data sequence as an observable sequence, commonly just called an observable. Observables help us manage asynchronous data, such as data coming from a back-end service. Observables treat events as a collection. We can think of an observable as an array whose items arrive asynchronously over time. A method in our code can subscribe to an observable to receive asynchronous notifications as new data arrives. The method can then react as data is pushed to it. The method is notified when there is no more data or when an error occurs. Observables are used within Angular itself, including Angular's event system and its HTTP client service, which is why we are covering them here. Observables allow us to manipulate sets of events with operators. Operators are methods on observables that compose new observables. Each operator transforms the source observable in some way. Operators do not wait for all of the values and process them at once. Rather, operators on observables process each value as it is emitted. Some examples of operators include map, filter, take, and merge. The map operator allows us to transform the incoming data. The argument to the map operator is an arrow function that says to take each data item and transform it to 10 times its value. So when we receive 1, it is mapped to 10, when we receive 2, it is mapped to 20, and so on. This depiction is called a marble diagram and is helpful for visualizing observable sequences. This is a screenshot from RxMarbles at this URL. We compose observable operators with the observable pipe method, hence the reason that observable operators are sometimes referred to as pipeable operators. Let's look at an example. Here, we see how to use the pipe method to compose multiple observable operators. RxJS has two primary packages we use to import functionality. Observable and the observable creation methods, such as range, can be found in the rxjs package. We import the operators from rxjs/operators. We use the range creation method to create an observable stream of numbers from 0 to 9. Not a common scenario, I know, but our goal here is to show how to compose operators with the pipe. By convention, we add a dollar suffix to the variables that hold an observable. This makes it easier to quickly distinguish the observables in our code, and we declare its type as an observable stream of numbers using the generic argument. We use the pipe method to pipe this observable stream through several operators, map and filter. We only include two operators here, but we can define any number of operators, separated by commas. This map operator takes each number emitted into the observable stream, 0 through 9, and multiplies it by 3, which results in an observable with 0, 3, 6, 9, and so on. As each number is emitted by the map operator, this filter operator filters the result to only the even numbers in the sequence. That is, those that when divided by 2 have a remainder of 0. Here we subscribe to the resulting observable to start receiving emitted values. The observable source does not emit any values until it has a subscriber. So, subscribing is key. What do you think will be logged to the console? Let's see how we got that result. The source emits 0, the 0 is multiplied by 3, resulting in 0. The 0 is divided by 2 with the remainder of 0, so it is included in the final result. The source then emits 1. The 1 is multiplied by 3, resulting in 3. The 3 is divided by 2, with a remainder of 1, so it is not included in the final result, and so on. Notice that each item in the sequence is processed through the pipeable operators as it is emitted. You may have worked with asynchronous data in JavaScript previously using promises. Observables are different from promises in several ways. A promise returns a single future value. An observable emits multiple asynchronous values over time. A promise is not lazy. By the time you have a promise, it's on its way to being resolved. An observable is lazy by default. Observables will not emit values until they are subscribed to. A promise is not cancelable, it is resolved or rejected, and only once. An observable can be canceled by unsubscribing, plus an observable supports map, filter, reduce, and similar operators. In this chapter, we'll do HTTP using observables.
We often encapsulate the data access for our application into a data service that can be used by any component or other service that needs it. In the last chapter, we did just that, but our product data service still contains a hardcoded list of products. We instead want to send an HTTP request to get the products from a back-end web server. Angular provides an HTTP service that allows us to communicate with a back-end web server using the familiar HTTP request and response protocol. For example, we call a get method of the HTTP service, which in turn sends a GET request to the web server. The web server response is returned from the HTTP service to our product data service as an observable. What does this look like in code? This is the ProductService we built in the last chapter, modified to retrieve the products using Angular's HTTP service. Let's walk through this code. First, we specify a URL to the products on the web server. This defines where we send our HTTP requests. Note that this URL is shown for illustration purposes only and is not a real URL. Next, we add a constructor. Recall from the last tutorial chapter that we use a constructor to inject dependencies. In this case, we need Angular's HTTP service, so we inject it here. Recognize the syntax? It creates a private HTTP variable and assigns the injected service instance to that variable. And since we are strongly typing this variable to HttpClient, we import HttpClient from the angular/common/http package here. Recall also from the last tutorial chapter that before we can inject a service in as a dependency, we need to register that service's provider with Angular's injector. The HTTP service provider registration is done for us in the HttpClientModule included in the angular/common/http package. To include the features of this external package in our application, we add it to the imports array of our application's Angular chapter, AppModule. Recall that the declarations array is for declaring components, directives, and pipes that belong to this chapter. The imports array is for pulling in external chapters. Now our Angular chapter illustration looks like this. We declare our components, we declare the directives and pipes that those components require, and we import the external chapters that we need. Going back to the product service, in getProducts, we use the injected http service instance and call the get method, passing in the desired URL. The HTTP service then sends a GET request using the specified URL. Often, the data returned from the back-end server is in JSON format. Lucky for us, the HTTP client get method makes it easy to receive that JSON data. We specify the expected type of response by setting the get method's generic parameter. Since we are expecting an array of product objects, we set the generic parameter to IProduct array. The get method then automatically maps the response object returned from the back-end server to the defined type, so we don't have to. Does this generic syntax look familiar? We used it earlier in this tutorial to define the event payload when passing event information from a nested component to its container. We aren't quite finished. What does our method now return? Since we are using strong typing, we should have a function return value here. We define the get method generic parameter as IProduct array. So will that be what we get back? Not quite. The getProducts method returns an observable of IProduct array. That's because the HTTP calls are asynchronous operations. And it's important to note that HTTP calls are single asynchronous operations, meaning that the observable sequence returned from the get method contains only one element. This element is the HTTP response object mapped to the type specified in the generic parameter. Notice that the observable also takes advantage of generics to define the type of data it is observing in the observable sequence. In our case, it's the array of products. Now let's add HTTP to our product service.
In this demo, we'll send an HTTP request to get the products for our Product List page. We are looking at our application's Angular chapter we called AppModule. Recall from the slides that Angular registers its HTTP service provider in an Angular chapter called HttpClientModule. In our application's Angular chapter, we import that HttpClientModule. We then pull that chapter into our application by adding HttpClientModule to the imports array here. Now we can inject the Angular HTTP service into any class that needs it. Here is the product data service we created in the last chapter with all of the hardcoded data. We want to modify our product service to get the product data using HTTP. Let's start at the top with the import statements. We want Angular to provide us an instance of the HTTP client service, so we identify it as a dependency in the constructor. We don't have a constructor yet, so let's add that first. Then we specify the parameter. Angular will then inject the HttpClient service instance into this variable. Now we need to identify the location of our web server. Hmm, this doesn't look like a valid URL to a web server, and to keep things simple, the demonstration reads the data from a local JSON file that was provided with the starter files. That way, we don't need to set up an actual web server. However, we do need to define the location of this JSON file so that the Angular CLI can find it when it serves up the application. I've already done this in the provided starter files. Here in the angular.json file, we can see the path here in the assets array. To change this code to work against a web server, simply change this URL to point to an appropriate web server, and of course, you need to write the server-side code to return the list of products. Ah, it's finally time to delete our hardcoded data, so let's delete the hardcoded products from the getProducts method. Gone. We'll call the HTTP get method here instead, passing in the defined URL. Since we expect the response to be a JSON structure containing an array of products, we set the get method generic parameter to IProduct array. When we get a response back, this method will then automatically map the returned response to an array of products. We'll need to change the return type as well. Now this method returns an observable of IProduct array. We can't try this out at this point because we are not yet subscribing to the observable returned from the service. Plus, looking at the console, we have a syntax error where we are calling this method since we changed the return type. Let's add some exception handling first, then modify the product list component to subscribe to the observable list of products.
As you can imagine, there are many things that can go wrong when communicating with a back-end service, everything from an invalid request to a lost connection. So let's add some exception handling. There are two key observable operators that we'll need. Tap taps into the observable stream and allows us to look at the emitted values in the stream without transforming the stream. So tap is great to use for debugging or logging. CatchError catches any error. We import them both from rxjs/operators. As we discussed earlier in this tutorial, to use these operators, we access the pipe method of the observable. We then pass in the operators, separated by commas. Here, the tap operator logs the retrieved data to the console. That way we can verify it's been retrieved correctly, and the catchError operator takes in an error handling method. The error handling method gets one parameter, the error response object. In the error handling method, we can handle the error as appropriate. We can send the error information to a remote logging infrastructure or throw an error to the calling code. Now let's add exception handling to our product service. We are back in the editor with the product service, just as we left it. This code is not really complete without the exception handling, so we'll add the appropriate imports for both the catchError and tap operators. We then call the pipe method and pass in both of the operators, just like we saw on the slide. I'll paste in the handleError method, and we need the import for HttpErrorResponse and throwError. In this method, we handle logging our errors any way we want. For our sample application, we'll just log to the console and throw an error to the calling code. So our getProducts method is complete. We can add other methods here to post or put data as well, but we still have that syntax error here, and we can't see the result of our hard work because we are not yet subscribing to the observable. Let's do that next.
Observables are lazy. An observable doesn't start emitting values until subscribe is called. So when we are ready to start receiving values in our component, we call subscribe. The subscribe method takes up to three arguments, each providing a handler function. The first argument is often called a next function because it processes the next emitted value. Since observables handle multiple values over time, the next function is called for each value the observable emits. The second argument is an error handler function, and yep, you guessed it, it executes if there is an error. In some cases, we want to know when the observable completes, so observables provide an optional third handler that is executed on completion. The subscribe function returns a subscription. We could use that subscription to call unsubscribe and cancel the subscription if needed. Now that our product data service is returning an observable, any class that needs product data, such as our product list component, can call our service and subscribe to the returned observable, like this. As we stated earlier, an observable doesn't start emitting values until subscribe is called, so this line of code calls the product data service getProducts method and kicks off the HTTP get request. It is then set up to asynchronously receive data and notifications from the observable. The first function passed to the subscribe method specifies the action to take whenever the observable emits an item. The method parameter is that emitted item. Since HTTP calls are single async operations, only one item is emitted, which is the HTTP response object that was mapped to our product array in the service. So the parameter is our array of products. This code then sets the local product's property to the returned array of products. The second function is executed if the observable fails. In this example, it sets a local errorMessage variable to the returned error. A third function, not used here, specifies the action to take when the observable ends with a completed notification. The third function is rarely used when working with HTTP requests, since they automatically complete after emitting the single response, and we aren't using the return value here since we have not provided an option for the user to cancel the request. Let's give this a try.
In this demo, we modify our component to subscribe to the observable provided by the product service. Here in the product list component, we see a syntax error. Type Observable of IProduct array is not assignable to IProduct array. Now that we've changed the product service to return an observable, we cannot assign the result to our product property directly. Rather, we subscribe to the returned observable. When the observable emits the data, we set our product property to the returned array of products. But things don't always go as expected. To handle any errors, let's add an errorMessage property. If our request for products fails, our errorMessage property is set to the error. What is this any syntax? This is a casting operator. We are casting the error returned from the observable to the any data type. Are we ready to try it out? Oh, we have no data. Why is that? Let's look again at our code. Recall that we said that HTTP is an asynchronous operation. What does that mean exactly for this code? Let's take it by the numbers. Angular first initializes the component and executes the ngOnInit method. We call the getProducts method of the product service. The product service returns an Observable of IProduct array. We subscribe to that observable and the HTTP GET request is submitted. This is the asynchronous operation. This code is then complete, and we execute the next line, which is setting our filtered products. But at this point, our products property is not yet set, so the filtered products is empty and we see no data. At some future point in time, the service receives the HTTP response from our request. The response data is mapped to an array of products and the observable emits that mapped data to any subscribers. Our subscriber receives the emitted data and assigns our product property to the emitted array of products. But since we are binding to the filteredProducts property, we are not notified that we now have the retrieved list of products. There are several ways we can handle this. One option is that we can move this line into the subscribe function, that way we won't assign the filteredProducts until the retrieved set of products are emitted from the service. Right now, the first argument passed into the subscribe method is a single line function. To pass in a multiple line function, we need to add curly braces, like this, then we can add additional lines into the function, like this line. Now our filteredProducts property is set after the products property is set to our list of products. Let's check it out. There are our products. Success. We have more products here now because we are retrieving them from the provided product.json file. Sweet. If we open the F12 Developer Tools, we see that data in the console, here. So the recommended way to use HTTP is to encapsulate it in a service, like our product service, then expose an observable for use by any class that needs product data. The class simply subscribes to the observable and waits for data or a notification. Let's finish up this chapter with some checklists we can use as we work with HTTP and observables.
Before we can use Angular's HTTP client service, some setup is required. We need to ensure that the service provider is registered with the Angular injector. This registration is done for us in the HttpClientModule, so all we need to do is pull the HttpClientModule into our application. We do this by adding HttpClientModule to the imports array of one of our application's Angular chapters. Build a data access service to wrap HTTP requests. In that data service, specify the needed imports, define a dependency for the Angular HTTP client service using a constructor parameter. Create a method for each HTTP request. In the method, call the desired HTTP method, such as get, and pass in the URL to the desired server. Use generics to specify the response return type. This will transform the raw HTTP response to the specified type. And do error handling as desired. And any class that needs data from a data service, call the subscribe method to subscribe to the observable. Provide a function to execute when the observable emits an item. This often assigns a property to the returned JSON object. And if that property is bound to a template, the retrieved data appears in the view, and add an error function to handle any returned errors. For more information on using HTTP with an Angular application, including create, read, update, and delete or CRUD operations, see the Angular: Reactive Forms tutorial. It demonstrates how to display, edit, and save data in Angular. For more information on observables and RxJS, see this Play by Play tutorial. This chapter was all about HTTP and observables. We began with an overview of observables and reactive extensions. We examined how to build a data access service that sends requests for data over HTTP. We walked through how to set up some basic exception handling, and we saw how to subscribe to the returned observable and ultimately display the resulting data in the view. We've now removed the hardcoded data from the product data service, yay, and instead retrieved the data using HTTP. In our sample application, we are using HTTP to retrieve the data from a local JSON file, but the techniques are the same for retrieving data from a back-end service. Next up, we'll see how to display multiple pages with navigation and routing.