How to create an Angular factory provider that allows Angular components and services to continue using AngularJS 1.x custom and built-in services during the upgrade process.
Introduction
The ngUpgrade library is awesome. It gives AngularJS developers everything needed to transform AngularJS 1.x applications to Angular applications iteratively.
Combine a fully-functioning AngularJS code base with an Angular code base to create a hybrid app – code using both Angular framework versions running simultaneously.
The library’s upgrade module transforms the process of upgrading an AngularJS application to Angular into a simple series of repeatable steps:
- Select a small part of the AngularJS application to upgrade [ex. Component or Svc]
- Rewrite that part as an Angular implementation
- Configure the application to share code regardless of its implementation framework with the upgrade module
- Repeat!
With the new code running right beside the old (original), it can immediately be verified that the original functionality is retained.
This process is fairly straight forward when applying to the AngularJS components, filters, directives, and services you’ve written.
But what about when it’s time to upgrade an AngularJS component that depends on one or more of AngularJS’ built-in services?
How can you write a new Angular implementation of a service or component that retains its ability to consume services like $timeout
, $location
, and $routeParams
?
Online Help to Upgrade from AngularJS
At first, it can be tricky to figure out how to approach some of the common development patterns needed to migrate each piece.
The documentation at angular.io presents a helpful guide entitled Upgrading from AngularJS. The article offers advice and examples for upgrading AngularJS applications to Angular. It includes things to consider prior to upgrading as well as instructions on how to use the ngUpgrade library to migrate an AngularJS 1.x application iteratively.
ngUpgrade Upgrading and Downgrading Terminology
Recently, I upgraded a couple of AngularJS applications in preparation for ngHouston’s Upgrade from AngularJS to Angular YouTube series.
While getting familiar with the ngUpgrade documentation, I got confused by its references to processes like upgrading a service and downgrading a component.
I now realize that it’s a matter of context. I was thinking only about upgrading the application. But the ngUpgrade library introduces phrases like upgrading a service and downgrading Angular components to describe how to use the upgrade module.
Those phrases describe the configuration and techniques used during an application’s life as a hybrid app.
As an application undergoes its upgrade, there should only be a single implementation of each application component. Each is implemented either in Angular or in AngularJS.
Upgrading an AngularJS Service or Component: tell Angular implementations (components, services) how to find AngularJS implementations.
Downgrading an AngularJS Service or Component: tell AngularJS implementations (templates, services, directives) how to find Angular implementations.
ngUpgrade Service Upgrade Patterns
There are two, complementary patterns to apply depending on whether the service is implemented in AngularJS or Angular.
Upgrading a Service
Preparing an AngularJS 1.x service for use by Angular components and services is upgrading a service.
Apply this pattern whenever refactoring or writing an Angular component or service that consumes a service still in its AngularJS 1.x form.
This is accomplished by adding an Angular factory provider definition. The factory provider defers requests from Angular components to the AngularJS $injector
.
Downgrading a Service
Preparing an Angular service for use by AngularJS 1.x components, directives, and services is downgrading a service.
Apply this pattern whenever an AngularJS component or service consumes a service written in Angular. That includes brand new services introduced to the application as Angular services as well as any services already rewritten in Angular during prior upgrade iterations.
This is accomplished by using the downgradeInjectable helper from the upgrade module to register the Angular service within the list of AngularJS-managed dependencies.
When to Apply the Upgrading a Service Pattern
While transforming an AngularJS component into its Angular implementation, all its service dependencies may not yet have been upgraded.
For example, imagine your application uses the AngularJS router to manage its pages. You’re just beginning to upgrade the application. You’ve upgraded a few services, but you haven’t touched routing yet.
Now, you’re ready to upgrade your first component. But the AngularJS router directs straight to that component when the user selects an item onscreen.
You won’t be able to upgrade that component without attention to routing. However, it would introduce a lot of risk if forced to update both the router and the component during the same upgrade iteration. You would lose the benefits of iterative updates if you attempted to tackle both the component and all the routing during the same iteration.
It would become difficult to ensure no functionality is lost if forced to upgrade large portions of the application. Since routing affects a large part of the application, it would be preferable to move it to Angular during the final stages of an upgrade.
So how could we continue using built-in AngularJS services like $location
, $routeParams
, and $timeout
alongside emerging Angular implementations?
Just apply the “upgrade a service provider” pattern.
Example Upgrade AngularJS to Angular Scenario
The AngularJS application has a couple views; one that lists Projects, and another one that provides an editor for detailed addition and editing of Projects. Those views are implemented as AngularJS component directives.
The editor supports both creating new Projects (the /new
route) and editing existing projects (the /edit/:projectId
route).
In the editor component, the projectId
provided by the route determines which Project is loaded into the editor.
The editor component is designed to navigate away from the editor and return to the list view whenever a successful save occurs.
The projectId is supplied via AngularJS $routeParams
service, while navigation between components is possible through the $location
service provider. Both are built-in AngularJS providers.
When I was ready to upgrade the “program-detail” component responsible for providing the editor and its functionality, I needed a way to continue using both $routeParams
and $location
from the new Angular implementation of the programDetail component.
Creating an AngularJS Upgraded Provider for Your Own Services
What are known as services in AngularJS are referred to as providers in Angular.
When we need to consume an AngularJS service from our new Angular component, we need to apply the “upgrading a service” pattern. That pattern allows us to introduce an Angular provider reference to the AngularJS service. Then, Angular components can find and consume the service per Angular convention.
The Upgrade from AngularJS article contains an example showing how to upgrade an application service. The example presumes you are the author of the “HeroesService” written in AngularJS.
Essentially, we introduce a new Angular provider definition that allows us to pull the AngularJS implementation of the service into Angular for use there, as well.
It is considered best practice to collect all Angular provider definitions used to upgrade AngularJS services into a single file named ajs-upgraded-providers.ts
. As you near the completion of your iterative upgrade process, you’ll be able to eliminate the dependencies on code within this file. Placing all declarations in a single file makes it easy to completely remove them from the project when ready.
Here are the steps to Upgrade a Service:
Steps to “upgrade an angularjs provider”:
- Create or open
ajs-upgraded-providers.ts
file - Import reference to AngularJS service implementation
- Define a factory function that uses AngularJS injector service to return an instance of the service
- Define an Angular factory provider that links the AngularJS service to that factory function and specifies the AngularJS
$injector
service as its lone dependency - Add the provider definition to the module “providers” list
Here’s what that looks like when creating an upgraded provider for the “projects” service:
ajs-upgraded-providers.ts
//step 2: import reference to AngularJS service implementatio
import { projects } from '../ajs-code/services';
//step 3: define factory function
export function projectsFactory(i: any) {
return i.get("projects");
}
//step 4: define Angular factory provider
export const projectsProvider = {
provide: RouteParams,
useFactory: routeParamsFactory,
deps: ['$injector']
};
app.module.ts
...
import { projectsProvider } from './ajs-upgraded-providers';
...
@NgModule({
...
imports: [
...,
UpgradeModule,
...
],
providers: [
...,
UpgradeModule,
projectsProvider, //step 5: add provider reference to module
...
],
...
]
})
export class AppModule {
constructor(private upgrade: UpgradeModule) { }
ngDoBootstrap() {
this.upgrade.bootstrap(document.body, ['project']);
}
}
Creating an AngularJS Upgraded Provider for Built-In AngularJS Services
But what about built-in AngularJS services? AngularJS dependencies are not available to Angular components automatically.
Unlike our own services, AngularJS does not provide a class definition to reference when following the previous steps. There’s no obvious way to define the Angular factory provider necessary to gain access to the AngularJS built-in services from Angular components!
The PhoneCat Upgrade Tutorial section of the Upgrade from AngularJS guide provides some help. It contains an example of what to do with dependencies on the $routeParams
service while upgrading the “phone-detail” component. The phone detail view uses the route to determine which phone’s details to display.
Although there are similar steps to those used when creating an upgrade factory provider for our own services, there are some differences in initial set up.
Since there is no service definition to import into the Angular environment, we need to create one.
Create an Angular provider reference to allow use of AngularJS service in our Angular component.
Here are the steps to Upgrade a Built-in AngularJS Service for:
- Create or open
ajs-upgraded-providers.ts
file - Define abstract class to represent the built-in AngularJS service
- Define a factory function that uses AngularJS injector service to return an instance of the built-in AngularJS service
- Define an Angular factory provider that links the AngularJS service to that factory function and specifies the AngularJS
$injector
service as its lone dependency - Add the provider definition to the module “providers” list
Here’s an example of how that looks when providing upgraded factory providers for the built-in $routeParms
and $location
AngularJS services.
ajs-upgraded-provided.ts
//step 2: define abstract class to represent built-in AngularJS services
export abstract class RouteParams {
[key: string]: string;
}
// important to use "any" to allow methods of upgraded service to be called without TS complaints
export abstract class LocationAjs {
[key: string]: any
}
//step 3: define factory function to return AngularJS service
export function routeParamsFactory(i: any) {
return i.get('$routeParams');
}
export function locationFactory(i: any) {
return i.get("$location");
}
//step 4: define Angular factory provider
export const routeParamsProvider = {
provide: RouteParams,
useFactory: routeParamsFactory,
deps: ['$injector']
};
export const locationProvider = {
provide: LocationAjs,
useFactory: locationFactory,
deps: ['$injector']
};
app.module.ts
...
import { routeParamsProvider, locationProvider } from './ajs-upgraded-providers';
...
@NgModule({
...
imports: [
...,
UpgradeModule,
...
],
providers: [
...,
UpgradeModule,
routeParamsProvider, //step 5: add reference to providers to Angular module
locationProvider,
...
],
...
})
export class AppModule {
constructor(private upgrade: UpgradeModule) { }
ngDoBootstrap() {
this.upgrade.bootstrap(document.body, ['project']);
}
}
General Steps to Upgrade an AngularJS Service by Creating an AngularJS Upgraded Provider
Here are general steps to upgrade an AngularJS 1.x service for use by Angular. Follow these steps for both application services you have written as well as for any services built-in to AngularJS.
- Create or open
ajs-upgraded-providers.ts
file - Provide a reference to the AngularJS service implementation by either:
- Import reference to your own AngularJS service
- Define abstract class to represent a built-in AngularJS service
- Define a factory function that uses AngularJS injector service to return an instance of the built-in AngularJS service
- Define an Angular factory provider that links the AngularJS service to that factory function and specifies the AngularJS
$injector
service as its lone dependency - Add the provider definition to the module “providers” list
Summary
The ngUpgrade library and its upgrade module provide developers with wonderful tools to transform AngularJS 1.x applications into Angular applications incrementally.
During the upgrade process, you may want to transform an AngularJS component into an Angular implementation prior to rewriting the AngularJS services on which it relies.
The ngUpgrade library allows us to apply the upgrade a service pattern to accommodate. The new Angular component can initially continue using services implemented in their existing AngularJS 1.x form.
Just create a new Angular factory provider reference to the AngularJS service. The same approach works both for services you’ve written and for the services built-in to AngularJS!
Next Steps
Looking for a detailed demonstration of how to iteratively upgrade an AngularJS 1.x application into an Angular implementation?
If so, check out AngularAir. In their ngAir 132 – AngularJS to Angular Part 2 screencast on October 3, 2017, you’ll see a full demonstration.
Don’t hesitate to contact me via Twitter (@DevelopingDenny) for any additional questions. I know the unexpected is bound to show up when you start upgrading your AngularJS app to Angular!