In our last guide, we covered how to install everything we need to get started with upgrading from AngularJS to Angular. We also covered how to rewrite and downgrade components.
In this guide, you will work with services in an ngUpgrade project. Specifically, you will:
Rewrite an AngularJS service to Angular
Convert an observable to a promise
Downgrade the service so it still works with our AngularJS code
Convert a promise to an observable
Take a minute to clone or fork this sample project on GitHub (don’t forget to run npm install
in both the public
and server
folders). Checkout this commit to see our starting point:
-
- git checkout 083ee533d44c05db003413186fbef41f76466976
-
We’ve got an Order System project that we can use to work through ngUpgrade. It’s using component architecture, TypeScript, and Webpack (with builds for both development and production). We’ve also got Angular and ngUpgrade set up and bootstrapped, and the home component has been rewritten to Angular.
(If you’re lost on any of that, we cover it all in the comprehensive video course Upgrading AngularJS.)
One quick note: Things change quickly in Angular and RxJS. If you’re using Angular 4.3+ or 5+, you’ll see a couple slight discrepancies here compared to the sample project. The sample project uses Http
in services for HTTP calls like GET
and POST
. We’re going to use the new HttpClient
that was added as of version 4.3+, which has the functionality required for the purposes of this tutorial… RxJS also made some changes as of version 5.5 in the way things are imported, so we’ll use that new style here.
When doing an ngUpgrade, it’s smart to pick one route at a time and work from the bottom up. You can take advantage of keeping Angular and AngularJS running side by side without worrying about breaking the app.
Since we did the home route in the last guide, we’re now ready to start on the customers
route. We’ll start by rewriting the CustomerService and downgrading it to make it available to our AngularJS components. Then, we’ll take a look at using both observables and promises in the service, so that you can choose for yourself which will work best for you in your migration process.
Before we rewrite the CustomerService, we have to explicitly import Angular’s HttpClientModule into our NgModule for the app (app.module.ts) in order to make HTTP calls. This is different than in Angular JS, where everything was included by default. In Angular, we need to be explicit about which parts of Angular we want to use. While it may seem inconvient at first, this is great because it helps reduce the footprint of our application by not automatically importing unused code.
So after line 3, we’ll import it like this:
import { HttpClientModule } from '@angular/common/http';
Then, we need to add that module to our imports
array after the UpgradeModule on line 12:
//app.module.ts
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
HttpClientModule
],
declarations: [
HomeComponent
],
entryComponents: [
HomeComponent
]
})
Now we’re able to use the HttpClientModule throughout our application. We only need to import it once and we can use it for all the rest of our services throughout the application.
Now that we’ve got HttpClientModule added to our Angular app module, we’re ready to rewrite the CustomerService in Angular. We’ll then downgrade it so that we can still use it in our Angular JS components as well as our Angular components.
The first thing we’ll do is rename the customerService.ts
file to customer.service.ts
so that it follows the current naming conventions.
Now, let’s open the file. You’ll see that we’re using an ES2015 class already:
//customer.service.ts
class CustomerService{
$http: any;
constructor($http) {
this.$http = $http;
}
getCustomers(){
return this.$http.get('/api/customers')
.then((response) => response.data);
}
getCustomer(id){
return this.$http.get(`/api/customers/${id}`)
.then((response) => response.data);
}
postCustomer(customer){
return this.$http.post('/api/customers', customer)
.then((data) => data);
}
}
CustomerService.$inject = ['$http'];
export default CustomerService;
Angular 2+ services are classes that we export, but we add the Injectable()
annotation. Gone are the days of trying to remember factories, services, providers, and how to create each one. In Angular, a service is a service, and it’s just an exported class with the injectable annotation. Isn’t that a huge relief?
The first thing we can do is delete the last two lines in this file. We no longer need the AngularJS $inject
array, and instead of using export default
, we’re going to add the export
keyword before the class declaration:
export CustomerService { //etc.
Now I’m ready to import two things from Angular up at the top of the file. The first is the Injectable()
annotation that was mentioned previously:
import { Injectable } from '@angular/core';
Next we need the HttpClient:
import { HttpClient } from '@angular/common/http';
Now we’re ready to make this an Angular service.
First, let’s add the Injectable()
annotation to our CustomerService, just above the class:
@Injectable()
There’s no options object that gets passed into this annotation.
The next thing we need to do is replace all of our references to AngularJS’s $http
service with Angular’s HttpClient. We’re going to use the shorthand http
for this instead, soperform a find and replace in this document, changing $http
to http
, given that most of the calls will largely be the same:
Now we need to change one thing about how our http property is created. Instead of this:
//customer.service.ts
class CustomerService{
http: any;
constructor(http) {
this.http = http;
}
…we’re going to delete line six that declares a public property of http
of type any
. Instead, in our constructor, let’s add the private
keyword before http
and specify that it’s of type HttpClient
:
//customer.service.ts
export class CustomerService{
constructor(private http: HttpClient) { }
With Angular’s dependency injection, we’re instantiating a private instance of the HttpClient service on our CustomerService.You can also see that, with the private
keyword, we don’t need to set our class instance of http
equal to our injected instance (it does this behind the scenes for us).
What we have now is the bare bones of an Angular service, but you’ll now see those red squiggly lines underneath our everywhere we use .then
. You can see that the IntelliSense is telling us that property then does not exist on type observable of response:
What’s going on there? Let’s tackle that next.
We’ve got our customer service largely rewritten to be an Angular service, but we’ve got a little bit of a problem with trying to use .then
on these http calls. That’s because the HttpClient in Angular returns an observable instead of a promise. We’ve got two choices here:
The practical way: convert these responses to promises and the rest of our application will work the same, or
The fun way: keep these responses as observables and update our components.
With any large scale refactor or upgrade, the goal is always to lose as little up time in your application as possible. The recommended approach is to first convert the calls to promises. That way, you can determine what components and other parts of the application are dependent on the service and its calls. After you’ve done that, you can convert the calls one at a time to observables, and update each component accordingly. So, first, get a service over to Angular and get it working. Then worry about using observables when you feel the time is right.
So let’s first convert the calls to promises. Don’t worry though - in a bit we’ll do the fun thing and convert a call to an observable.
To convert observables to promises, we first need to import from RxJS, the library that handles observables. After our Angular imports, we just need to add:
import { Observable } from 'rxjs/Observable';
This lets us use various functions for the observable object provided by RxJS.
The toPromise
method lets us convert observables to promises. It used to be a separate import in previous versions of RxJS, but has now been rolled into Observable
. Importing individual operators is a common pattern in RxJS, but figuring out which operators you need and where they reside in the library can be a little daunting. Be sure to go through the documentation resources that RxJS provides, as well as the Angular documentation on RxJS.
Now we can use the toPromise
operator before each .then
in our calls. When you do that, you’ll also see an error that says that .data
is not a property that exists on the type “object”. That’s because the response already returns the data object inside of the HTTP response. All we need to do then is remove the .data
. This is different than in the days of the original Http
service, where we needed to call a .json
function to return the data.
One more thing. Since we have the benefits of TypeScript, let’s add the return type to each of these functions. It’s always best in TypeScript to specify types when possible, even though, technically, it’s not required. So, after each function name, we’ll add :Promise<any>
.
The finished functions in the service will look like this:
//customer.service.ts
getCustomers():Promise<any> {
return this.http.get('/api/customers')
.toPromise()
.then(response => response);
}
getCustomer(id):Promise<any> {
return this.http.get(`/api/customers/${id}`)
.toPromise()
.then(response => response);
}
postCustomer(customer):Promise<any> {
return this.http.post('/api/customers', customer)
.toPromise()
.then(data => data);
}
With that, we’ve successfully converted the observables in our calls to promises.
Now that we’ve converted our observables to promises, we’re ready to downgrade the customer service so that the not-yet-migrated AngularJS components can still use it.
This process is similar to when we downgraded the home component in the previous guide. The first thing we need to do is import the downgradeInjectable
function from the ngUpgrade library, just like we imported downgradeComponent
for the home component. So after line two, we’ll add:
import { downgradeInjectable } from '@angular/upgrade/static';
We also need to declare a variable called angular
just like we did in our home component. So after line four, we’ll add:
declare var angular: angular.IAngularStatic;
Then at the bottom of our file, we’ll register our service as a downgraded factory. So, after the end of the class, we’ll type:
angular.module('app')
.factory('customerService', downgradeInjectable(CustomerService));
We’ve downgraded the CustomerService to be available to AngularJS. Here’s the finished service:
//customer.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { downgradeInjectable } from '@angular/upgrade/static';
declare var angular: angular.IAngularStatic;
@Injectable()
export class CustomerService {
constructor(private http: HttpClient) {}
getCustomers():Promise<any> {
return this.http.get('/api/customers')
.toPromise()
.then(response => response);
}
getCustomer(id):Promise<any> {
return this.http.get(`/api/customers/${id}`)
.toPromise()
.then(response => response);
}
postCustomer(customer):Promise<any> {
return this.http.post('/api/customers', customer)
.toPromise()
.then((data) => data);
}
}
angular.module('app')
.factory('customerService', downgradeInjectable(CustomerService));
Our customer service has been rewritten to an Angular service and downgraded to be available to AngularJS. Now we need to remove our reference in our AngularJS module and add it to our Angular module.
First, let’s open up our AngularJS module (app.module.ajs.ts
). You can remove line 22:
import CustomerService from './customers/customerService';
…as well as line 41:
.service('customerService', CustomerService)
Those are all the changes you need to make in this module.
Now let’s add our service to our NgModule
in app.module.ts
so that our Angular code can access it. The first thing we need to do is import the service after line seven:
import { CustomerService } from './customers/customer.service';
Now to register our customer service in our application, we need to add an array of providers
to our NgModule
after our entryComponents
array and add our CustomerService there:
//app.module.ts
providers: [
CustomerService
]
The providers
array is where we’ll register all of our services in the application. And now we’ve got our customer service registered in our NgModule and ready to go.
This method of downgrading – registering the downgraded service in the service file and removing it from the AngularJS module file – works perfectly well for development or if you plan on quickly rewriting your application before you deploy. However, the Angular AOT compiler for production won’t work with this method. Instead, it wants all of our downgraded registrations in the AngularJS module.
The downgrade is identical, but instead you’d:
Import downgradeInjectable
in app.module.ajs.ts
(you’ve already got angular
in there so you don’t need to declare it).
Change the import of CustomerService
to import { CustomerService } from './customers/customer.service';
since we switched to a named export.
Change the service registration to the exact same factory registration shown above.
We’d better make sure our application is still running. Let’s start our Express API, then run our Webpack development server. Open a terminal and run these commands to start Express:
cd server
npm start
Then open another terminal and run these commands to start Webpack:
cd public
npm run dev
You should see everything compile and bundle correctly.
Now, open a browser and head over to localhost:9000
. Let’s navigate to our customers route and see if the service is working:
We can double-check that we’re using the rewritten Angular service by going to the sources tab in the Chrome developer tools, navigating down to the customers folder, and clicking on the CustomerService source:
This shows our rewritten service. We’ve updated the service to Angular, but it’s being used in both the customers component and the customer table component, both of which are still in AngularJS.
Now that we’ve got the CustomerService downgraded and working, let’s have some fun and use that getCustomers
call as an observable. That way we can start taking advantage of all the new features of observables. This is going to be a little bit tricky, because we’re using the call in both the customers component and the orders component, neither of which have been rewritten to Angular yet. Don’t worry - I’ll show you step-by-step how to do this.
Back in the customer service code, the first thing that we need to do is change the return type on line 16 to Observable<any>
. Of course now, TypeScript is complaining to us because we’re converting toPromise, so we just need to delete both the toPromise
and then
functions. It looks like this now:
getCustomers():Observable<any> {
return this.http.get('/api/customers');
}
Now we need to update our customers component to use an observable instead of a promise. We’ll do that next.
Our getCustomers
call is now returning on observable. Let’s update our customers component (customers.ts
) to use an observable instead of a promise. The customers component is still an AngularJS component and that’s fine, we don’t need to mess with it yet, but let’s use a little TypeScript to help us out. Let’s import our CustomerService at the top of our file:
import { CustomerService } from './customer.service';
Now that we’ve imported the CustomerService, we can specify the type of our injected CustomerService in our controller function definition:
//customers.ts
function customersComponentController(customerService: CustomerService){
We now have the advantage of TypeScript complaining about our .then
just like it did in our CustomerService. It knows that the getCustomers
call is supposed to return an observable and that .then
doesn’t exist on an observable.
The way we use an observable in a component, whether it’s an AngularJS or Angular component, is to subscribe to it. Before we can subscribe to this observable, we need to import Observable
just like we did in the service. So, above our CustomerService import, we’ll add:
import { Observable } from 'rxjs/observable';
This will let us use functions on observable, including subscribe
. So, now on line 18 inside of our $onInit
function, we can just change the then
to subscribe
, and everything else can stay the same.
Let’s go look at the browser and see if this worked as expected. If you head over to the customers route, you should see that everything is working the same. However, if we go over to the Orders tab, we see a big problem: no data and TypeError: Cannot read property 'fullName' of undefined
in the console. What’s going on here?
It turns out the orders component also uses the getCustomers
call, but it’s still trying to use it as a promise. Let’s fix that.
When we rewrote our getCustomers call to be an observable instead of a promise, we accidentally broke our orders component (orders/orders.ts
), which is still in AngularJS. That’s because in our $onInit
function, we’re using $q.all
to wait for two promises to return before we assign any of the data to our view model:
vm.$onInit = function() {
let promises = [orderService.getOrders(), customerService.getCustomers()];
return $q.all(promises).then((data) => {
vm.orders = data[0];
vm.customers = data[1];
vm.orders.forEach(function (order) {
var customer = _.find(vm.customers, function (customer) {
return order.customerId === customer.id;
});
order.customerName = customer.fullName;
});
});
};
This was a common pattern in AngularJS.
One solution to this problem would be to just rewrite the orders component to Angular, and also rewrite the order service. But, in the real world, that’s just not always possible right away. Remember, in any large-scale refactoring, the first priority is to make sure we minimize downtime and be able to have a continuously deliverable application that we can always deploy to production.
However, what if the orders component was much more complicated and we didn’t have the time to rewrite it? In that case, we have two choices: we can either convert our getCustomers
call to a promise in the orders component, or we can convert the getOrders
promise to an observable.
To convert getCustomers
to a promise in the component, we’d just do exactly the same thing we did earlier in the service - import Observable
from RxJS and add the toPromise
operator after getCustomers
. It’s that easy, and it’s a handy trick if you just can’t don’t have time to refactor this component to use observables quite yet. However, it’s not completely desirable, as our long-range goal is to completely get rid of promises and switch entirely to observables. So, Iet’s convert our getOrders
call to an observable here.
getCustomers
to a PromiseLet’s convert the getOrders
to an observable. The first thing we’re going to do is import our CustomerService at the top of the file just like we did in the customer component:
import { CustomerService } from '../customers/customer.service';
Then we can specify the type of our injected CustomerService in our controller function definition:
//orders.ts
function ordersComponentController(orderService, customerService: CustomerService, $q) {
In order to convert the getOrders
call to observable, we’re going to use two static methods on observable called fromPromise
and forkJoin
. The fromPromise
method lets us convert a promise to an observable, and forkJoin
lets us subscribe to multiple observables. So, you might have guessed by now that the first thing we need to do is import those two methods at the top of our file:
import { fromPromise } from 'rxjs/observable/fromPromise';
import { forkJoin } from 'rxjs/observable/forkJoin';.
Now we can do some work in our $onInit
function. Above line 21, let’s to declare a variable called ordersData and use the fromPromise method:
let ordersData = fromPromise(orderService.getOrders());
Now let re-write $q.all
to use forkJoin
instead. So, first we’ll just replace return $q.all
with forkJoin
. We need to pass in an array, so let’s move the promises
array and add ordersData
to the front of it and then just get rid of the promises
declaration. Lastly, let’s change .then
to .subscribe
just as with a single observable. Here’s our finished $onInit
function:
vm.$onInit = function() {
let ordersData = fromPromise(orderService.getOrders());
forkJoin([ordersData, customerService.getCustomers()]).subscribe((data) => {
vm.orders = data[0];
vm.customers = data[1];
vm.orders.forEach(function (order) {
var customer = _.find(vm.customers, function (customer) {
return order.customerId === customer.id;
});
order.customerName = customer.fullName;
});
});
};
Let’s recap what we’ve done here. First, we called fromPromise
and converted our getOrders
call from a promise to an observable. Then, we used forkJoin
to subscribe to both the ordersData
and the getCustomers
call. Just like with $q.all
, the subscribe for forkJoin
will return an array of our data in the order that we’ve listed them. So, data[0]
will be our order, and data[1]
will be our customers.
Let’s do one more thing to clean this up. We can remove the $q
dependency from line 16 in our $inject
array and line 167 in our function definition.
Let’s go look at the browser one more time and make sure this worked. You should see that our application compiles and loads correctly, so check out the orders tab:
This shows that our data is loading correctly. Now you’ve seen how to translate back and forth between promises and observables, which is useful when you’re working on a large application where you can’t just convert everything to observables all at once as you’re upgrading.
From here, use this guide and the last one to convert the customersTable
component and the products
route. You’ll need to learn a few new tricks with Angular’s template syntax, but otherwise you’ll have everything you need.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!