NgUpgrade in Depth

Victor Savkin is a co-founder of Nrwl. We help companies develop like Google since 2016. We provide consulting, engineering and tools.
NgUpgrade is a library put together by the Angular team, which we can use in our applications to mix and match AngularJS and Angular components and bridge the AngularJS and Angular dependency injection systems. In this article we will look at what it is and how it works.
How to Use it Best
This article only talks about the mechanics of of the library, and not how to use it in the best way. That’s covered in the rest of the series.
How it Works
An Angular application is a tree of components, with each of them having an injector. Plus there is an injector for the NgModule of the application. When trying to resolve a dependency for a particular component, the framework will first try to get it from the component tree. And if the dependency cannot be found, Angular will get it from the NgModule’s injector.
If the application uses lazy loading, the diagram will look more complex, but we will cover this use case in the article about lazy loading and NgUpgrade.
Using NgUpgrade we can bootstrap an existing AngularJS application from an Angular application. And we do it in such a way that we can mix-and-match components written in the two frameworks and bridge the two DI systems. We can consider such an application a “hybrid application”.
Bootstrapping
The easiest way to bootstrap such a hybrid application is to have an NgModule without any bootstrap components. Instead it defines ngDoBootstrap where we use the injected UpgradeModule to bootstrap the AngularJS application.
This is a good default because components upgraded from AngularJS to Angular require an AngularJS ancestor component, and this way of bootstrapping guarantees that. This, however, does not work in certain situations. For instance, it won’t work if we load and bootstrap the AngularJS application lazily, only when the user navigates to a certain route. In this case we need to bootstrap our hybrid differently, by doing it in a lazy-loaded component.
UpgradeModule.bootstrap
UpgradeModule.bootstrap has the same signature as angular.bootstrap. And if we look at the implementation, we will see that it actually calls angular.bootstrap under the hood, but it does it with a few tweaks:
- It makes sure angular.bootstrap runs in the right zone.
- It adds an extra module that sets up AngularJS to be visible in Angular and vice versa.
- It adapts the testability APIs to make Protractor work with hybrid apps.
Capturing AngularJS and Lazy Loading
One thing that may not be obvious is that importing “@angular/upgrade/static” captures window.angular. And that’s why we have to import the AngularJS framework before we import “@angular/upgrade/static”.
Otherwise we’ll see the “AngularJS v1.x is not loaded” error.
This works well for simple applications, but is problematic for complex enterprise applications, where, for instance, AngularJS can be loaded lazily via requirejs. To make it work there, we can manually reset AngularJS, as follows:
Using “‘@angular/upgrade/static” or “@angular/upgrade”?
For historical reasons NgUpgrade has two entry points: “@angular/upgrade” and “@angular/upgrade/static”. Use “@angular/upgrade/static”. It provides better error reporting and works in the AOT mode.
Fixing Module Resolution
Depending on our application’s build setup, we may need to point the bundler to the right UMD bundle. For instance, this is how you do it for webpack:
We have learned how to bootstrap a hybrid application. Now let’s see how we can bridge the AngularJS and Angular dependency injection systems.
Dependency Injection
An important part of the upgrade process is moving services (or to use a better word, “injectables”) from AngularJS to Angular. Usually we don’t have to make any of the changes to the injectables themselves — we just need to make sure they are properly wired up in the DI system. This often requires us to either access AngularJS injectables in Angular or access Angular injectables in AngularJS.
Accessing AngularJS Injectables in Angular
Say our AngularJS application has the following injectable:
We can access it in the Angular part of the application via $injector, like this:
UpgradeModule brings in $injector (the injector of our AngularJS application), and that’s why we can access it in AppModule or any of its children.
Note that $injector gets defined by the upgrade.bootstrap call. If we try to get it before calling bootstrap, we will see the “Cannot read property ‘get’ of undefined” error.
Accessing Angular Injectables in AngularJS
We can also make Angular injectables available in the AngularJS part of our application, like this:
There is more going on here. This is because the Angular DI system allows us to use any token (e.g., types) to express dependencies between injectables. But in AngularJS we can only use strings for that. By doing this m.factory(‘angularInjectable’, downgradeInjectable(AngularInjectable)) we map AngularInjectable to the ’angularInjectable’ string. We then use it to instantiate needsAngularInjectable.
Component Integration
Another important capability provided by NgUpgrade is being able to mix-and-match AngularJS and Angular components.To demonstrate how it works, we’ll look at the following example.
There are three components in this example: AppComponent > angularJSComponent > AngularComponent.
- AppComponent is written in Angular and is downgraded to AngularJS.
- angularJSComponent is written in AngularJS and is upgraded to Angular.
- AngularComponent is written in Angular and is downgraded to AngularJS.
Let’s start with a simple case where the components do not pass any data and do not re-project any nodes.
Let’s look at this example in detail.
First, to use an Angular component in an AngularJS context we need register it using the downgradeComponent function, like this:
This creates an AngularJS directive with the appRoot selector. This directive will use AppComponent to render its template. Because of this indirection, we need to register AppComponent as an entry component.
So the <app-root> element itself is owned by AngularJS, which means that we can apply other AngularJS directives to it. Its template, however, is rendered using Angular.
The downgradeComponent function sets everything up in such a way that the AngularJS bindings will be hooked up with the inputs and outputs of AppComponent.
Extending UpgradeComponent allows to upgrade an AngularJS component.
To ensure the necessary guarantees, NgUpgrade allows only for certain directives to be upgraded. So if we have a directive defining compile, terminal, replace, or link.post, we will have to wrap it into an AngularJS component.
Inputs and Outputs
Now, let’s make components interact via inputs and outputs.
Let’s go through the changes.
Adding inputs and outputs to a downgraded component does not require any extra configuration — we just need to add the properties themselves.
And we can bind to them in the AngularJS context.
Note that as with Angular templates, we use square brackets and parenthesis. But in opposite to Angular, we have to hyphenize property names.
When upgrading components we have to to list the inputs and outputs in two places.
First in the Angular directive extending UpgradeComponent.
And then in the AngularJS component itself.
Two-Way Bindings
AngularJS and Angular implement two-way binding behavior differently. AngularJS has a special two-way binding capability, whereas Angular simply uses an Input/Output pair. NgUpgrade bridges the two.
Bindings and Change Detection
Change detection works differently in AngularJS and Angular. In AngularJS $scope.apply triggers a change detection run, also known as a digest cycle. In Angular, we no longer have $scope.apply. Instead, the framework relies on Zone.js, which will trigger a change detection run on every browser event.
Since a hybrid application is an Angular application, it uses Zone.js, and we don’t need to worry about $scope.apply.
Angular also provides strict guarantees to make the order of checks predictable. A hybrid application preserves these guarantees.
Transclusion/Reprojection
Both AngularJS and Angular provide ways to project nodes from the content DOM into the view DOM. In AngularJS it is called transclusion. In Angular is is called reprojection.
For simple cases like this, everything works the way you would expect. You use <ng-transclude></ng-transclude> in AngularJS and <ng-content></ng-content> in Angular. Multi-slot reprojection still has some issue, which hopefully will be ironed out soon.
Code Samples
- The code samples illustrating the bridging of the two dependency injection systems.
- The code samples illustrating the component integration.
Summary
NgUpgrade is a library we can use to mix and match AngularJS and Angular components and bridge the AngularJS and Angular dependency injection systems. In this article we looked at how it works and how we can use it.
Upgrading Angular Applications Book
This article is based on the Upgrading Angular Applications book, which you can find here https://leanpub.com/ngupgrade. If you enjoy the article, check out the book!
Victor Savkin is a co-founder of Nrwl. We help companies develop like Google since 2016. We provide consulting, engineering and tools.
If you liked this, click the 👏 below so other people will see this here on Medium. Follow @victorsavkin to read more about monorepos, Nx, Angular, and React.