written by PrimeHammer

AngularJS to Angular Upgrade Process (Part 2)

This is a second article in the series of AngularJS to Angular Upgrade Process where we describe the Angular Upgrade Module and bootstrap a hybrid application.

If you are still wondering, why you should at least consider upgrading your AngularJS application to Angular, take a look at the previous article here. In the previous article, we’ve covered the preparation steps before the upgrade process itself. In this one, we’ll take a look at the Angular Upgrade Module and we’ll bootstrap a hybrid application.

Note: Angular 1.X was officialy renamed to AngularJS and Angular 2+ is just called Angular.

Dirty hands

We’ll get our hands dirty

The Upgrade Module

The Upgrade Module is an official part of Angular framework itself. It allows you to mix and match AngularJS and Angular components within the same application. This module is the core unit in the upgrade process itself.

Your application can run both versions of Angular at the same time with the Upgrade Module. Angular framework is responsible for Angular components and AngularJS for AngularJS components. These components do not run in an emulated environment. You can use all features of both frameworks and expect natural behavior. On top of using Angular components in AngularJS application, components and services of one framework can communicate with those from other framework.

One common question that may arise is:  how does the Upgrade Module cope with Dependency Injection (DI)? Dependency injection is one of the core concepts both in AngularJS and Angular, but there are some significant differences in its usage. AngularJS expects the DI tokens to be always strings while Angular can benefit from TypeScript and use different types, classes or strings as well. AngularJS uses exactly one injector, therefore everything is contained in one big namespace. Angular uses a hierarchy of injectors, which correspond to the hierarchy of components. One can imagine that there is a specific injector for each component, even though the implementation differs slightly. In spite of these differences you can use AngularJS services in Angular by upgrading them and Angular services in AngularJS by downgrading them. Note that only services from the root injector can be downgraded.

If we took a look at the DOM of a hybrid application, we would see components and directives of both frameworks. These components communicate via their input and output bindings. The Angular framework ignores AngularJS components as well as directives and vice versa. The root and entry point of a hybrid application is always handled by AngularJS. AngularJS templates can use Angular components. If we want to use some <ng2-component></ng2-component> inside a AngularJS template, we can attach AngularJS directives to that component, because it is used inside AngularJS template. Yet, we cannot use Angular directives on that component. The same rules apply the other way around.

<!-- ng1-template.html -->
<div class="foo">
<ng2-component ng-repeat="car in cars"></ng2-component>
<!-- This won't work -->
<!-- <ng2-component *ngFor="let car of cars"></ng2-component> -->
</div>

In the previous article we discussed$scope.$apply() and its absence in Angular framework. AngularJS change detection runs every time you or the framework calls the $apply() method. Angular change detection works in a different way. The change itself can be caused by either some DOM event (click, input, dblclick, etc.), AJAX request or by a Javascript timeout. The one common thing for all of these options is that they are asynchronous. Zones allow you to wrap asynchronous code in a so-called execution context and register some event after all asynchronous calls are done. In our case this would be registering a call to Angular change detection.

If you are wondering how this magic works, I strongly suggest reading this article. To make a long story short: zone.js monkey patches all asynchronous events, e.g., setTimeout, addEventListener, XMLHttpRequest and connects them to the execution context. Inside the monkey patched counterparts, it stores the reference to the particular execution context and calls appropriate lifecycle hooks when needed.

In case someone wonders, how this monkey patching thing works, here’s some really high-level overview of this approach:

//zone-example.js
//type - string, e.g., 'click'
//listener - callback
function zoneAddEventListener(type, listener) {
    listener();
    notifyAttachedZone(Zone.current);
}
window.prototype.addEventListener = zoneAddEventListener;

After we slightly alter the handling of asynchronous operations, we are able to notify the current zone, that one of the async tasks is done. After all these tasks are done, zone just calls the Angular change detection for us.

Back to our hybrid application. Since the asynchronous code is being monkey patched, everything that could trigger a change in a hybrid application runs inside the zone. This applies to both AngularJS and Angular parts of the application. The Upgrade Module will invoke $rootScope.$apply() after every call to the Angular zone. The practical implication is that we don’t need to call $apply() ourselves. The Upgrade Module will do that for us.

Bootstrapping a hybrid application

So far, we’ve talked about the Upgrade Module and the concept of a hybrid application where you can use both AngularJS and Angular at the same time. But how can we achieve this? First, you need to install typescript as your devDependency. Once it isinstalled you should open a terminal and run typescript with the watch option: tsc –watch. Many AngularJS applications already provide some kind of development stack, such as Gulp or Grunt. There are solutions which enable you to integrate Typescript compilation into your existing dev stack. Typescript compiler will automatically watch .ts files and run compilation after every change to those files. The next step is to install Angular. My advice is to create a new Angular application with angular-cli and compare the package.json file of the Angular application to the one in our AngularJS application. Then we can just add all the Angular dependencies that were missing in the AngularJS package.json file, delete the node_modules directory and run npm install.

Now weneed to move the index.html file to the project root directory and adjust the development server root path appropriately. After that, we are able to serve everything from the root directory. However, if your AngularJS application lives in another directory, you can specify a special <base href=”/path/to/dir”> tag in your index.html file. The browser uses this path to prefix all relative URLs when referencing CSS, script and image files.

We are now ready to get systemjs package up and running. SystemJS is a module loader which enables us to dynamically load Javascript modules as needed. It will take us another step closer to the Angular application. We will load the Angular root module via SystemJS later. Before we include SystemJS, we need to import core-js shims, to be able to use the latest Javascript features and zone.js, which will take care of the Angular change detection for us.

//index.html
<head>
…
<script src="/node_modules/core-js/client/shim.min.js"></script>
<script src="/node_modules/zone.js/dist/zone.js"></script>
<script src="/node_modules/systemjs/dist/system.src.js"></script>
<script src="/systemjs.config.js"></script>
<script>
System.import('/app/main.js');
</script>
</head>

We also need to create the SystemJS configuration file:

System.config({
    paths: {
        // paths serve as alias
        'npm:': '/node_modules/'
    },
    map: {
        app: '/app',
        /* . . . */
        '@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
        /* . . . */
    }
})

The SystemJS module loader will now look for the app/main.js file as an entry point. We will soon put some content in that file. First,  we need to create the AngularJS and Angular root modules. Rename the app.module.js to app.module.ajs.js and change the file reference in the index.html as well. Now create an app.module.ts file with the minimal NgModule class definition. Import the BrowserModule (the default module for the browser) and UpgradeModule.

//app.module.ts
import {
    UpgradeModule
} from '@angular/upgrade/static';
@NgModule({
    imports: [
        BrowserModule,
        UpgradeModule,
    ],
})
export class AppModule {
    ngDoBootstrap() {}
}

The last step is required to properly implement the bootstrap process. AngularJS allows you to bootstrap the application in two ways. The first is to add the ng-app directive with the name of the application to the root element. The second is to call the angular.bootstrap method with the root element and the name of the application as arguments. Angular only supports the latter, so if you are using the ng-app directive, you need to remove it. After that, we are ready to add the final piece to bootstrap the application:

//main.ts
import {
    platformBrowserDynamic
} from '@angular/platform-browser-dynamic';
import {
    UpgradeModule
} from '@angular/upgrade/static';
import {
    AppModule
} from './app.module';
platformBrowserDynamic().
// Bootstrap the Angular application
bootstrapModule(AppModule).
// Then bootstrap the AngularJS application
then(platformRef = & gt; {
    const upgrade = platformRef.injector.get(UpgradeModule) as UpgradeModule;
    // This was previously done via
    upgrade.bootstrap(document.documentElement, ['phonecatApp']);
});

Conclusion

In this article, we have explored the Upgrade Module, especially how it handles the Dependency Injection and Change Detection differences. We have covered the process of bootstrapping a hybrid application and explained the different bootstrapping approaches. In the next article, we’ll try to use the Upgrade Module to upgrade some AngularJS dependencies in order to use them in Angular and downgrade some Angular dependencies to use them in AngularJS.

Leave a Reply

Your email address will not be published. Required fields are marked *