AngularJS vs. Angular in 2023 - When and How to Migrate Your App?

Mateusz Wrzaszcz, Olaf Krawczyk

angular2

Getting into a project based on Angular.js in its 1.4 version is not the most exciting thing that can happen to a programmer in 2023. And it doesn’t matter whether you’re a junior front-end developer or a senior engineer—working with AngularJS simply feels like 2014 all over again (which feels like an eternity ago in JavaScript years).

Lately, we were approached by a client who intended to migrate to the latest version of Angular. 

Now, THIS was something.


Our team was quite excited by this perspective. Working with the most cutting-edge tools and technologies is always an opportunity to grow as professionals and to dive into all the latest tech solutions which is just pure fun.

We do prefer to start projects from scratch and choose the setup and technology that suits our liking and project requirements. But that wasn’t the case this time around. We had to keep building upon the existing AngularJS application for a few months and work on fixing bugs at the same time.

The reasons were obvious: the application’s end users expected new features as well as performance improvements.

We decided on incremental refactoring using a “hybrid” approach. And below you’ll find an outline of what exactly went into the process hiding behind the popular buzzword.

Cta image

A Hybrid Approach Using AngularJS and Angular

First, let’s back up a little.

To reconcile app expansion with fixing bugs, we decided that half of the project's frontend team would work on new features and the other half would migrate the old components to the new framework. At this point, we needed to deal with three critical aspects:

  • the process had to be incremental and we had to work through component after component, service after service, and so on.
  • the application couldn’t stay frozen during the migration process as we needed to deliver new features
  • most importantly, no new features would be developed using legacy code.

Running two frameworks side by side sounded like a solution. The migration could be done collaboratively and spread over time—it seemed like a win-win situation!

There are some viable alternatives to AngularJS, but we ultimately chose Angular as the heir apparent. Unanimously. The decision was driven primarily by Angular’s official support of the “ngUpgrade” module and its stellar documentation.

The transition was even easier because of TypeScript—it has been doing a great job in the current tech stack up to that point and would work well with modern Angular, too.

The migration process was not all roses though. Below, we’ll share some of the experiences of the process and provide a handful of working solutions that helped us through. Hopefully you’ll find it useful when migrating your app from AngularJS to Angular.

Initial State: AngularJS 1.4

We started with AngularJS 1.4 with TypeScript and Gulp as task runners. We also had some Bower dependencies and Typings for type definitions. Both are deprecated, however, and have been for some time.

AngularJS to Angular Migration

The task before us consisted of the following steps:

  1. Moving to Webpack
  2. Creating an Angular project structure and adding dependencies
  3. Preparing Webpack configuration for hybrid app
  4. Bootstrapping the hybrid app
  5. Bringing SASS and Pug to the party
  6. Hybrid app (the first component downgraded)
  7. UI Router

Step 1: Moving to Webpack

As step one, we decided to find a promising replacement for Gulp. Currently, there are two popular task runners out there—SystemJS and Webpack—and we took both into consideration. After some discussions, our development team ultimately chose to go with Webpack.

The reasons behind our decision included sufficient complexity and flexibility to run AngularJS 1.4 and then smoothly handle the new Angular.

Webpack not only handles module bundling but can also pack our application and run a dev server. And it can do that for both Angulars equally well. The team's experience also contributed to the decision. Webpack configuration mostly depends on your application’s specification, but you can find a lot of pre-made configurations on GitHub.

Instead of diving into a comprehensive rundown of our complete configuration, we’ll outline its most important parts instead.

The opening section our Webpack configuration defines the entry property and includes two files: our AngularJS initialization file and the file that collects all global styles. 

entry: [
  './src/app/bootstrap.module.ts',
  './src/app/index.scss'
],

The rules section, which contains a loader for TypeScript and SCSS files, is another important part. Depending on your needs, it also should include HTML and asset loaders.

module: {
  rules: [
    {
      test: /\.tsx?/,
      exclude: /node_modules/,
      loader: 'ts-loader'
    },
    {
      test: /\.scss$/,
      use: [
        MiniCssExtractPlugin.loader,
        'css-loader',
        'resolve-url-loader',
        'sass-loader'
      ]
    }
    // e.g. 'html-loader', 'file-loader' 
    // and many more depending on your project needs
  ]
},

Step 2: Creating an Angular Project Structure and Adding Dependencies

Setting up an Angular project without Angular CLI can be complicated.

CLI tools are awesome and starting a new project with CLI takes only one command. Sometimes, however, we have to get our hands dirty and start a project without neat tools.

Guess what? For us, creating a hybrid app was one of those times.

We had to start the Angular project within an already existing AngularJS environment. Before setting up a new project structure, we added Angular dependencies to our existing project.

Here’s a part of package.jsonthat shows all the required dependencies.

"@angular/common": "^7.0.3",
"@angular/compiler": "^7.0.3",
"@angular/core": "^7.0.3",
"@angular/forms": "^7.1.0",
"@angular/platform-browser": "^7.0.3",
"@angular/platform-browser-dynamic": "^7.0.3",
"@angular/router": "^7.0.3",
"@angular/upgrade": "^7.0.3",
"rxjs": "^6.3.3",
"zone.js": "^0.8.26",
"core-js": "^2.5.7",

With all the dependencies installed, we then moved to create the structure of application.

After considering a few possible hybrid application structures, we ultimately decided to put all the files related to the Angular application in one directory to separate them from the AngularJS project.

This is what our project structure looked like:


    ├── app
    │   ├── index.module.ts
    │   ├── index.route.ts
    │   └── index.scss
    ├── assets
    │   ├── icons
    │   └── images
    ├── index.html
    ├── ng-app
    │   ├── app.module.ts
    │   └── main.ts
    └── styles

We didn't want to change anything in the app directory, so it contains just the AngularJS application as you can see above. The ng-app folder holds all the files related to the new Angular project.

Separating both frameworks allowed us to keep the project structure clean. Components rewritten to new Angular could be easily removed from the old application, which really helped us keep the project organized.

Having installed all dependencies and created the project structure, the next step was to add a minimal configuration for the Angular application.

To bootstrap the Angular project, we had to create two files.

One to declare AppModule and the other to import dependencies like polyfills and later the Angular package, and bootstrap the application.

app.module.ts file:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

@NgModule({
  imports: [BrowserModule ]
})

export class AppModule {
  constructor() {
    console.log('Angular 7 is running!');
   }
}

main.ts file:

// Polyfills
import 'core-js/es6';
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';

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

platformBrowserDynamic().bootstrapModule(AppModule);

With this done, we could move to the next step which was configuring Webpack to run our newly created app.

Step 3: Preparing Webpack Configuration For a Hybrid App

Having Webpack up and running and the Angular project created, it was time to configure Webpack to build the new project.

To accomplish that, we needed two additional loaders: ts-loader (which we already had) and angular2-template-loader.

The first one helps loading TypeScript files, the other allows us to load external Angular template files. Both should be installed as a dev dependency npm install --save-dev ts-loader angular2-template-loader. The next step was to add a rule to the webpack.config.jsthat would allow us to load TypeScript files and Angular templates. It should look something like this: 

{
  test: /\.tsx?/,
  exclude: [/node_modules/, /\.(spec|e2e)\.ts$/],
  include: path.resolve(__dirname, 'src/ng-app'),
  loader: ['ts-loader', 'angular2-template-loader']
}

Because our Angular project was located in the ng-app directory, we instructed Webpack to search for Angular .ts files only there and omit the AngularJS folder.

The last thing to change in the Webpack configuration was the entry point. It had to point to the file where the Angular AppModule was bootstrapped. In our case, it was main.ts.

Step 4: Bootstrapping the Hybrid App

After setting up all the dependencies and creating the startup files, we were finally ready to start our AngularJS project inside the Angular application.

We needed to import the AngularJS app in our main.ts module, so it would be visible to the Angular app (e.g. angular.module('webapp', [ .. ]);`) and instruct Angular to bootstrap this module via ngUpgrade:

export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  
public ngDoBootstrap() { this.upgrade.bootstrap(document.getElementById('appMainController'),
['webapp'], { strictDi: false }
); }
}

This step requires some additional configuration, and comprehensive instructions can be found on the official ngUpgrade page.

Our AngularJS application had angular-deferred-bootstrap set up. This component provides a function which resolves before app start.

It can be used to collect data from the backend, the configuration for example. Unfortunately, this third-party library wasn't working with ngUpgrade.

The new Angular provides a similar mechanism called the APP_INITIALIZER token. It allows us to connect to the bootstrap process and perform some action, talking to the backend, for example.

The tricky part was to pass that data to AngularJS and utilize it there.

To do so, we created a service that talks to the backend and collects the required configuration. Then this service had to be downgraded to AngularJS. The ngUpgrade module provides an appropriate static method called downgradeInjectable. Sample usage in AngularJS:

import { configService } from '../ng-app/shared/services/config.service';
// ...
.service('configService', downgradeInjectable(configService) as any)

Step 5: Bringing SASS and Pug to the Party

We use SASS in the old AnuglarJS application and we wanted to use it with the new framework, too.

It’s much more powerful than pure CSS and lets us use variables, mathematical operations, mixins, loops, imports, and other interesting features that make writing styles easier and more fun.

To do so, we had to use Webpack plugins—raw-loader and naturally sass-loader. Apart from installing those packages, one additional step still needed to be taken.

We had to exclude the ng-app directory from the global SASS-loader rule and create a new one just for Angular:

// Part of webpack.config.js
// Angular SASS rule
{
    test: /\.scss$/,
    include: path.resolve(__dirname, 'src/ng-app'),
    loaders: ['raw-loader', 'sass-loader']
},

// Global SASS rule
{
    test: /\.scss$/,
    exclude: [path.resolve(__dirname, 'src/ng-app')],
    use: [
        {
            loader: 'css-loader',
            options: {
                sourceMap: true,
                minimize: process.env.NODE_ENV === 'production'
            }
        },
        'resolve-url-loader',
        {
            loader: 'sass-loader',
            options: {
                sourceMap: true
            }
        }
    ],
},

We decided that we didn’t want to write classic HTML for the sake of readability. There are many template engines available out there, but PUG seems to be a proven solution and we already have a track record with it.

It has no problems with Angular’s specific attributes and its configuration with Webpack is easy as well.

The next to last thing for us to do was install dependencies: 

yarn add -D pug pug-loader apply-loader

And then, in a final touch, we expanded webpack.config.js by adding loaders:

    {
        test: /.pug$/,
        loader: ['apply-loader', 'pug-loader'],
    },

Step 6: Hybrid App (First Component Downgraded)

Once the hybrid app was up and running, we were ready to start the process of upgrading code.

One of the most common patterns for doing that is to use an Angular component in an AngularJS context.

This could be a completely new component or one that was previously AngularJS but has been rewritten for Angular.

The ngUpgrade module provides an appropriate static method called downgradeInjectable, the same we used earlier during the configService implementation.

 import { downgradeComponent } from '@angular/upgrade/static';

Let’s say we had a simple Angular component:

import { MonteNewAngularComponent } from './test.component';

If we wanted to use this component from AngularJS, we would need to downgrade it using the downgradeComponent() method. As a result, we’d get an AngularJS directive, which you can then register in the AngularJS module:

.directive(
  'monteNewAngular',
  downgradeComponent({ component: MonteNewAngularComponent }) as angular.IDirectiveFactory
);

Step 7: UI Router

Downgrading or upgrading components is not the only way to successful migration.

We found out that UI Router that was already there could just as well help us with our task. It can switch logic between AngularJS and new Angular thanks to its support for hybrid apps.

The angular-hybrid module enables UI Router to route to both AngularJS and Angular components. And that’s exactly what we needed!

We followed the steps described in the UI Router documentation to make adjustments to our hybrid app. In AngularJS, we just added one module in our dependencies:

yarn add @uirouter/angular-hybrid

Then, in our AppModule, we had to import this module:

import { UIRouterUpgradeModule } from '@uirouter/angular-hybrid';

@NgModule({ imports: [ ... UIRouterUpgradeModule.forRoot(), ... ], ...
})

We followed it up with removing old dependencies from the project:

import 'angular-ui-router';

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

And replaced them with new dependencies:

import uiRouter, { UrlService } from '@uirouter/angularjs';

angular.module('monteApp', [uiRouter, 'ui.router.upgrade']);

Another important thing was to tell UI Router that it should wait until all bootstrapping is complete before doing the initial URL synchronization:

.config(['$urlServiceProvider', ($urlService: UrlService) => {
      return $urlService.deferIntercept()
    }]);

The next step involved telling UI Router to synchronize the URL and listen for further URL changes during the process of bootstrapping AngularJS:

import { NgZone } from '@angular/core';
import { UrlService, UIRouter } from '@uirouter/core';

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .then(platformRef => {
    // Intialize the Angular Module
    // get() the UIRouter instance from DI to initialize the router
    const urlService: UrlService = platformRef.injector.get(UIRouter).urlService;
    // Instruct UIRouter to listen to URL changes
    const startUIRouter = () => {
      urlService.listen();
      urlService.sync();
    };

From this moment onwards, our router was ready to serve both types of components. We decided that this was a much better way rather than upgrading or downgrading components. Then the moment came to create our first route.

It turned out we had to import StateProvider and UrlRouterProvider to a router config file from our upgraded uirouter dependency:

import { StateProvider, UrlRouterProvider } from '@uirouter/angularjs';

Also we need to import component we want to connect to the state:

import { MonteNewAngularComponent } from './monte/monte-new-angular.component';

And so we were ready to add a new state definition that would provide either an Angular or AngularJS component view. This is what our sample state looked like:

.state({
  name: 'monteComponent',
  url: '/monte',
  component: MonteNewAngularComponent
})

Summary

There you go. If you chose Angular as a successor to your AngularJS app, we hope this article will help you with the transition process. Things to do in this project:

    • Split large and complex controllers into modern, lightweight components,
    • Build a modern architecture with a separated services layer to properly apply your business logic.
    • Rearrange the project file structure to improve readability and boost development pace.

We hope this short guide will help you migrate your app from Angular.js to Angular should you decide to do so. 

If you have any questions or comments, don't hesitate to contact us:

Cta image
Mateusz Wrzaszcz, Olaf Krawczyk avatar
Mateusz Wrzaszcz, Olaf Krawczyk