Tutorial

Migrate Your AngularJS Services to Angular with ngUpgrade

Published on December 12, 2019
Default avatar

By Sam Julien

Migrate Your AngularJS Services to Angular with ngUpgrade

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.

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

Our Starting Point

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:

  1. 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.)

Upgrading Angular JS header image

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.

Rewriting an AngularJS Service

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.

Adding HttpClient to NgModule

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.

Rewriting the Customer Service

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?

Preparing the Code

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.

Updating the Service Class to Angular

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:

Example of "Property does not exist" warning in the form of red squiggly line

What’s going on there? Let’s tackle that next.

Converting Observables to Promises

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:

  1. The practical way: convert these responses to promises and the rest of our application will work the same, or

  2. 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.

Using the toPromise Operator

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.

Downgrading the Customer Service

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));

Moving the Service to the Angular Module

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.

Removing Content from the AngularJS 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.

Moving the Service to Angular 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.

A Quick Note on AOT Compiling

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.

Testing the Application’s Functionality

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:

Customers route example

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:

Chrome developers tools sources tab

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.

Using GetCustomers As an Observable

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.

Using an Observable in the Customers Component

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.

Fixing the Orders Component

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.

Converting getCustomers to a Promise

Let’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.

Confirming that the Application is Working

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:

Successful Orders tab example

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.

Conclusion

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.

Upgrading AngularJS header image

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Sam Julien

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


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!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel