Migrate AngularJS to Angular using UpgradeModule (Part 1)

Danny Cornelisse
08-10-2018

Incremental refactor using hybrid approach

The purpose of this blog is to lay out a strategy to refactor an angularJS app to an Angular app. Although they are similar in name, these frameworks are quite different, with the main difference being the inclusion of Typescript in Angular, which allows type-checking of variables, objects and properties. Despite the differences in the two frameworks, they still share some of the functional concepts that had made angularJS so popular and successful: services, components, property bindings, dependency injections and a modular structure. Therefore, the angular team has provides a guide to upgrade from angularJS to Angular:

https://angular.io/guide/upgrade

The approach laid out here builds on top of this guide, but differs slightly, because it requires less preparation compared to the approach in the link above. This alternative approach is applicable to angularJS apps that haven’t migrated to webpack as a build tool and also is suited for angularJS apps that are not written in ES6 modules (js files that have imports and exports), but rather use an IFEE (Imediately Invoked Function Expression) structure. In other words, this alternative approach has fewer pre-requisites than the first approach.

 

The concept

NgUpgrade

As mentioned above, the refactoring will be done using the ngUpgrade module provided by the angular team. This is an Angular module and is available as a npm package. That means that it can only be used by an Angular app. However, this module allows angularJS components and Angular Components to be used side-by-side, as a hybrid app. Or as the Angular team says:

[…] what you’re really doing is running both angularJS and Angular at the same time. All Angular code is running in the Angular framework, and angularJS code in the angularJS framework.

The major advantage is that old angularJS components can still be used in the Angular app, while Angular components and services can be downgraded to angularJS components and services. This makes dependency injection still possible.

Used Tools

The used tool for the hybrid app is the Angular-CLI, the command line interface for Angular apps. This is where this approach differs from the approach given by the angular team. While the Angular-CLI has integrated webpack and typescript, they advocate setting up webpack (or any other module loader) and typescript manually. This is very hard to do and will only slow the whole process down. Additionally, it is very hard to build a good webpack config, while Angular-CLI already provides an optimized webpack config suited for almost any app with a wide array of build options. Therefore it is suggested to introduce the Angular-CLI regardless of what the angular team suggests.

 

Prerequisites

angularJS style guide and component structure

The angularJS needs to have been written according to the John Papa style guide. This is essential. Constrollers should use the this keyword (assigned to const vm = this; variable) rather than $scope. This will make refactoring angularJS controllers to Angular Components way easier. The same goes for angular services. Furthermore, it is advisable to use angularJS components rather than just controllers. This also makes it easier to use the angular-router or the ui-router.

Ng-annotate

Before doing anything, the angularJS files need to be checked if they use the $inject method (Injection Function Annotation). Each function needs to be annotated if the javascript code has to be minimize and uglify’d. There are two ways to do this:

  1. Manually: manually annotate al angularJS services, controllers, directives, run blocks and config blocks
  2. Use a build tool to automate annotation. Use gulp-ng-annotate or ng-annotate. Make sure to annotate the source files, and not the build files because the Angular-CLI does not support annotation.

 

Strategic order for refactoring

Introduction angular-cli

To use the Angular-Cli, install it globally using npm:

npm install -g @angular/cli

create an Angular-cli app using:

ng new <project-name>

There are two options:

1: Create an Angular app outside of the repository:

/my-app
   /src
     /app
       app.module.js
   package.json // old
/src
  /app
    app.module.ts
  main.ts
angular-cli.json
package.json // new

 

2: Create an Angular app directly in the repository:

/src
 /app
    app.module.ts
  /my-app
    /src
      /app
        app.module.js
      package.json // old
  main.ts
angular-cli.json
package.json // new

Both approached need restructuring of the folders. Recommended steps to do:

  1. Make sure the .git folder is in the right directory. This points to the repository.
  2. Combine the two package.json files. It’s easier to copy/paste the packages from the old package.json to the new package.json, where the angular-cli dependencies are listed. This requires running npm install

 

Removing legacy build tools

The angular-cli comes with build-in build options using the ng build command. If the previous build tool was grunt or gulp, look very carefully at the specified tasks. If there are tasks that are not supported by angular-cli, consider running grunt and gulp alongside the angular-cli build tool. It is possible to use npm scripts in package.json to change build commands to include old tasks:

// package.json

"scripts": {
    "ng": "ng",
    "start": "webpack-dev-server --port=4200",
    "build": "webpack",
    "my-task": "webpack && gulp annotate",
  }

> npm my-task

 

Introduction ngUpgrade module

In this chapter, the hybrid app will be created. This will be done using the ngUpgrade module. Install it via npm:

npm install @angular/upgrade –save-dev

Run this in the directory where angular-cli is installed. The Angular-cli has created an index.html file. If there is an index.html  in the project, they have to merged now, or later. In the angularJS index.html  there probably is the ng-app directive implemented, with optionally a ng-strict-di directive:

<body ng-app="myApp" ng-strict-di> … </body>

What these directives do is to bootstrap the angularJS app. With Angular upgrade, do this manually using the UpgradeModule:

// app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
 
@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['myApp'], { strictDi: true });
  }
} 

Then in main.ts file (this is the entry file for the built-in webpack), bootstrap the Angular app:

// main.ts

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule, [])
  .catch(err => console.log(err));

Lastly, add this to app.module.ts:

declare var angular: angular.IAngularStatic;
import { setAngularJSGlobal } from '@angular/upgrade/static';
setAngularJSGlobal(angular);

What this does is setting angularJS as the angular global, provides that it have added it to the global scope (window) of the app. This can be done in “scripts” in angular-cli.json:

"scripts": [
  "./../node_modules/jquery/dist/jquery.min.js",
  "./../node_modules/angular/angular.min.js",
  "./../node_modules/angular-route/angular-route.min.js"
],

Import angularJS files as modules

In this part the angularJS will be imported as if they are files of type “module”. This is done using the require.context method from webpack. (https://webpack.js.org/guides/dependency-management/#require-context).  Use the following lines to import all .js files from a directory and all its subdirectories:

// main.ts

declare const require: any;

const context = require.context('./app', true, /\.js$/);

context.keys().forEach((file: any) => {
    try {
        context(file);
    } catch (err) {
        console.log(err, file);
    }
});

This is where the approach in this document differs from the upgrade guide by the angular team. With the code above files that do not have exportable members because they are scoped using IIFE’s can still be added to the bundle.js that webpack generates. This saves a lot of refactoring of angularJS files to modules and ultimately makes the prerequisite to use a module loader (https://angular.io/guide/upgrade#using-a-module-loader) unnecessary.

Import templates in angularJS components

To be able to use template urls in old angularJS components, they need to be imported in the *.component.js files.

import * as template from './login-modal.html';

(function () {
    'use strict';
    angular
        .module('app')
        .component('loginModal', {
            template: template,
            // templateUrl: '../app/header/login-modal/login-modal.html',
            controller: 'LoginModalController',
            controllerAs: 'LoginModalCtrl',
            bindings: {},
            require: {}
        });
})();

This needs to be done manually, because the old templateUrl property of the component points to the location of the template. However, because of webpack these templates are added to the bundle.js, and are not available on the (local) server.

 

An alternative is to add these html files as assets in the assets folder and require them from there. Another option is to use another build tool like gulp or grunt to copy them to the dist  folder.

Import external dependencies

Because the app probably has external dependencies, they need to be imported in the app. That can be done in two ways:

1: Using the scripts array in angular-cli.json, add the scripts. This makes them available to the global scope. Do this only when the node module has no module support:

// angular-cli.json

"scripts": [
  "./../node_modules/jquery/dist/jquery.min.js",
  "./../node_modules/angular/angular.min.js",
  "./../node_modules/bootstrap/dist/js/bootstrap.js",
  "./admin-lte/js/adminlte.js",
  "./../node_modules/angular-route/angular-route.min.js"
],

2: Importing them in a file. This has preference to avoid polluting the global scope!

// app.module.ajs.ts

import '@uirouter/angularJS';
import 'angular-route';
declare var angular: angular.IAngularStatic;


angular.module('app', [
    'ui.router'
]);


export default angular.module('app');

The two code blocks above show how to import angularJS dependencies. If the app has an app.module.js file, refactor it to Typescript as shown above and rename to app.module.ajs.ts. this indicates that this is the module for the angularJS app. Then import it in the app.module.ts:

// app.module.ts
import {default as app} from './app.module.ajs';

Lastly, remove any script tags from the index.html file that reference any scripts (app or vendor). The angular-cli will add the scripts instead.

Bootstrap the hybrid app

Now everything should be ready to be run as a hybrid app. Run ng serve to start the app. If any problems occur, please refer to this document or the angular team upgrade guide:

https://angular.io/guide/upgrade

Also useful:

https://scotch.io/tutorials/get-started-with-ngupgrade-going-from-angularJS-to-angular

https://blog.nrwl.io/ngupgrade-in-depth-436a52298a00

 

LEAVE A REPLY

you might also like