Upgrade from AngularJS to Angular
You might already know that AngularJS is nearing its end of life. In 2018, the product entered its Long Term Support (LTS) period. This period was slated to end at the end of June 2021, now postponed until December 31st, 2021 due to COVID-19. Come 2021, the AngularJS team will stop supporting the framework completely, including critical security fixes. It is time to think about options to update applications that are using AngularJS.
In this article, I’ll share insights on migrating from AngularJS to Angular 8 (latest version at the time of writing is 8.2.14), including:
how to determine whether to migrate, rewrite, or updating hybrid,
5 steps to prepare for migrating,
7-step migration process,
how to migrate incrementally,
and extra steps to enhance the migration plan.
As Angular authors have amazed the development community with rapid releases of Angular 9 and Angular 10 frameworks, these newer versions also introduced some breaking changes. Therefore, Angular 8 applications that are subject to be upgraded even further should be evaluated on a case-by-case basis. The information in this article is not exhaustive; the configuration and code covered are from a “work in progress” state.
NOTE: While the team originally worked to migrate AngularJS application to Angular 7 (v7.2.12), this approach is still valid for Angular 8. This text may include some online references to Angular 7.
Evaluating alternatives to migrating: Rewrite or update?
There are a lot of factors that go into determining the best migration strategy for your team. A logical starting point is to first decide whether the application needs to be completely rewritten from scratch or can it be updated incrementally. There are quite a few routes to take with a long list of considerations. There are even full courses on the topic.
A critical facet of migration is the framework. Frameworks may or may not support JavaScript, TypeScript, or both while others support JSX. The development team may be more familiar with another framework/library or have specific preferences. What are the limitations on what can and cannot be used? How much time and resources are needed to accomplish the migration? The list could go on and on.
There are other options outside of migration to consider, such as rewriting or updating with a hybrid approach. The section below provides details on these alternatives.
Rewriting legacy applications
This approach gives developers the freedom to basically do whatever they want. You can use any framework. You can re-design and re-engineer the application structure or even improve the look and feel and functionality of the application. While the benefits are obvious, there are some additional aspects to consider before opting to rewrite the application from scratch.
Time and money: Recreating an application doesn’t happen overnight. Change takes time and funding to cover the cost of a heavier workload.
Staff and release management: The larger the app, the more skilled people are needed. For some time, you might have to manage the simultaneous development of two applications.
Support and features: There’s a large chance that while the new application version is being implemented, you also have to support the existing one and maybe even implement new features in it. New features then have to be ported to the new application too.
Rewriting works best when the application is not very big. Then, developers "freeze" development efforts of an old application and focus on creating the new one. While rewriting doesn’t work for all projects, I suggest taking this route whenever possible because of the flexibility the option offers.
Updating with a hybrid approach
Another tactic is to use a hybrid approach, rewriting parts of the application incrementally. The new code lives with the old until the old code is fully rewritten. With this approach, it’s important to map out how the app migration will be carried out, who will be migrating old features, who will be implementing new features, and how the new elements will work together.
Additional considerations:
You have to figure out how to integrate AngularJS with a new framework/tool. The new framework and tooling used will have different concepts and ways of doing things.
You need to get rid of the old code as fast as possible. The goal from the start is to migrate away from AngularJS and not create a half-AngularJS and half-some-other-framework monster. The longer your app remains in a hybrid state with two code bases running at the same time, the harder it is to maintain. Even framework runtime code sizes add up, and you end up serving multiple megabytes of JavaScript code to users' browsers.
You should try to implement new features using a new framework as much as possible. Otherwise, you end up with even more elements to migrate later.
Migrate to Angular
In my particular case, migrating to Angular made the most sense. However, there are other options (e.g., Vue.js has ngVue, and with React, you could look into react2angular). You can access more information on alternative options here. The content below notes my recommendations for preparing and the process for migrating.
5 steps to prep
Based on experience, there are some steps to take before the actual migration starts. The information in this section is not mandatory, but it may help with the migration process. The sooner you prepare for future migration, the easier your life will be when migration starts.
1. Set up TypeScript.
Technically Angular does not need TypeScript to work. (It works with JavaScript and even Dart.) However, if you decide to use TypeScript, you could set it up and use it in advance. With TypeScript ready, this gives you one less thing to worry about when planning for the actual migration.
@types/angular should help you set it up. You'll also have to make your TS and JS code interoperable. Luckily, TS compiler helps do that quite easily. --allowJs compiler option allows JS files to be compiled using TS compiler.
Additionally, you should look into a TS module integration with the module system used by your project. Our framework uses CommonJS, including --module="commonjs" and --esModuleInterop=true.
TSC configuration for JS interoperability should look like this:
{
"allowJs": true,
"module": "commonjs",
"esModuleInterop": true
}
*Introducing TS requires some decisions to be made, which are covered in the "Babel vs. TSC" paragraph below.
2. Tidy up code structure.
Consider using the AngularJS style guide if, of course, you're not already using it. While the guide is old and a lot of things have changed in JavaScript ecosystem, you don't need to follow this style guide to the letter. However, you do still find great advice regarding AngularJS best practices and code structure. I recommend "Rule of 1." The clear division helps manage and migration plan effort.
3. Introducing AngularJS components.
AngularJS 1.5 introduced Component API. If possible, try to use it as much as possible because this API is like the API provided in Angular. Making this move likely means less code needs to be rewritten.
*More details about these and other preparation steps can be found in the official documentation.
4. Avoid $scope.
Avoid introducing new instances of $scope and start refactoring out existing dependencies on $scope. Angular doesn't have the concept of $scope anymore. The fewer dependencies on $scope the better (i.e., an easier migration). Using AngularJS components helps because the API reduces the need for and discourages usage of $scope.
5. Use the ngUpgrade library.
Even though Angular is not backward compatible with AngularJS, it provides an official way to ease step-by-step migration. The ngUpgrade library provides tools to mix and match AngularJS and Angular code inside a hybrid application. Both frameworks are started, code is executed in a framework for which it is written (i.e., AngularJS code runs in AngularJS framework and Angular code runs in Angular framework), and tools provided by ngUpgrade connect the components in both frameworks to interoperate with minimal setup from the developer. The ngUpgrade is a big help with its documentation covering the main parts of the migration process.
*For details on ngUpgrade and migration process, check out the official documentation or the ngMigration forum.
7-step migration process
Inevitably, issues that are not covered in the official documentation start to come up. In this section, I’ll go over the seven steps our team outlined to migrate successfully. I'll describe the situations we encountered when setting up ngUpgrade. Hopefully, these insights help provide more context on what to expect for your migration.
1. Build the application.
Historically, our team used Webpack and Gulp to build an application. However, Angular included CLI tools to build, run, test, lint, etc. The question came up, "Should we migrate to Angular CLI?" CLI provided a predefined list of commands for all the things offered.
At first glance, this wouldn’t work for our project. We had a hybrid application with elements in play that were necessary outside of the Angular ecosystem. Even though Angular CLI supports customization via so-called builders, we decided not to introduce Angular CLI to the project and kept using Webpack. (Fun fact: Angular CLI uses Webpack internally.)
There were a couple of reasons for this decision. First, we were using Angular 7, and Angular CLI builders were officially supported with Angular 8. Second, we didn't want to introduce even more changes and complexity to the migration process by implementing existing tasks in Angular CLI. We knew we could always go back and move to Angular CLI in the future, especially when the Angular version was going to be bumped to 8+.
Note: We used Webpack 4, and the entire configuration was prepared for this version. If you are using an older version, your configuration might differ a little. Webpack documentation should help you with that. Also, you might need to upgrade it if some plugins/loaders require that or find alternatives for them.
For more details, check out the following articles on customizing Webpack configurations in Angular and configuring Webpack 4 with Angular 7.
2. Configure template loading.
Angular and AngularJS use HTML templates. In a hybrid application, you need to support loading templates for both frameworks. In some cases, you might need to separate loading these HTML files.
For example, our project used AngularJS Template loader to preload templates. Angular didn’t work with templates processed via this loader. To resolve the issue, we split up the AngularJS application source code and stored it inside two directories: /public and /shared. Newer Angular code had designated /src directory where its code was stored. As a result, configuring Webpack to use different loaders for different paths was easy.
{
test: /\.html$/,
include: [
resolvePath("src")
],
exclude: /index\.html$/,
use: {
loader: "html-loader",
options: {
attrs: false
}
}
}, {
test: /\.html$/,
include: [
resolvePath("public"),
resolvePath("shared")
],
use: [{
loader: "ngtemplate-loader",
options: {
relativeTo: `${__dirname}/`
}
}, {
loader: "html-loader",
options: {
attrs: false
}
}]
}
Just by adding two rules for HTML files and including (see included property) different paths for them, you can easily set up different loader chains for Angular and AngularJS templates. Webpack supports other configuration options you could try to implement similar behavior.
3. Choose between Babel and TSC.
When introducing TS, we had to decide what compiler/transpiler to use. In our specific case, Babel was already used in the project. Babel also offers more flexibility, configuration, and extensibility options than the TypeScript compiler. These two points alone seemingly justified its use. However, there was a major reason for migrating to TSC instead: the Angular Ahead-of-Time compiler only supports TSC. While you can live without AOT, it ultimately helps improve application size and rendering performance. After considering the pros and cons, we decided to use TSC.
4. Prepare TS compiler for Angular.
Angular relies heavily on TS decorators. For decorators to work, you need to enable two TS compiler features.
{
"experimentalDecorators":true,
"emitDecoratorMetadata":true
}
experimentalDecorators enable decorators.
emitDecoratorMetadata emits decorator data in compiler code. This data is needed by Angular to work.
As all other TS compiler configuration options, these settings are specified in tsconfig.json file.
5. Set up browser support.
When setting up an Angular application, read the browser support guide. Set up required polyfills depending on your needs.
6. Bootstrap hybrid applications.
Set up bootstrapping. For details on how to bootstrap Angular and AngularJS parts of the application, refer to the official guide.
7. Adjust routing.
For AngularJS application, you’re likely using the default routing mechanism, which is supported by ngUpgrade. But what if you are using UI-Router? Turns out that UI-Router has a solution for hybrid applications. We ended up using UI-Router “hybrid” in our application.
If you are using UI-Router events and don’t want to refactor your code, you can enable events explicitly. It is an easy two-step process:
Import "@uirouter/angularjs/release/stateEvents".
Add "ui.router.state.events" with other UI-Router dependencies to the AngularJS app module dependencies.
Example code is shown below.
const uiRouter = require("@uirouter/angularjs").default;
const { upgradeModule } = require("@uirouter/angular-hybrid");
require("@uirouter/angularjs/release/stateEvents");
let dependencies = [uiRouter, upgradeModule.name, "ui.router.state.events"];
let ngModule = angular.module("app.core.routing", dependencies);
Executing the migration incrementally
I spent about two weeks working on the hybrid. Another developer who helped with the initial TS setup spent about a couple more days on it. In retrospect, it would only require about one week of my effort to perform the same tasks using the information provided in this article.
Overall, the whole process should take around 2-3 weeks for an experienced software engineer. This includes different approach research and analysis, as well as becoming familiar with Angular framework.
Split the whole migration process into steps and execute them one by one. For example, try taking the below steps:
Add TS support and make sure it is working (see 5 steps to prep).
Introduce the Angular framework and set up ngUpgrade (see 5 steps to prep).
Set up routing (see 7-step migration process).
Set up AOT (which was not available at the time our team has worked on this code base - see below).
Upgrade testing and linting infrastructure (setup will vary case-by-case - see below).
Executing steps one at a time makes troubleshooting easier. If something goes wrong, you know the exact part you’ve been experimenting with and can easily find the culprit.
Looking at more migration measures
There are some elements still pending in our migration process. This section details the aspects that our team intends to add in the future.
i18n
The old application used angular-translate. The closest equivalent for Angular is ngx-translate. These libraries seem similar with ngx-translate, potentially a viable replacement for angular-translate. If using AngularJS native translation features, Angular also has its own implementation: https://angular.io/guide/i18n. Also, ngx-translate might be deprecated in favor of Angular native solution. Details can be found here.
AOT
In addition to official documentation, this article is another helpful resource to set up AOT and provide rationale for choosing AOT over JIT.
Source path resolution
While not completely related to Angular migration, our team used source path resolution in our application. In TS config, we set up path mapping. If you want the same functionality, use the tsconfig-paths-webpack-plugin to handle it.
Testing
Testing a hybrid application is complex and is not a “one size fits all” solution. Thanks to GitHub, there’s a great article that covers how to test hybrid apps that include angular.js components. For more details on Angular testing, check out https://angular.io/guide/testing.
Styling
In our AngularJS application, styling code was separated from component code. Angular can work in this way but also offers the possibility to use component-specific styles. Consider this option to make components in your application even more isolated.
Linting
In a hybrid application situation, the linting setup might not be a trivial task. You might be using ESLint. Angular CLI uses TSLint by default. Keep in mind there are plans for TSLint to be deprecated. Beware: there might be some extra work involved for the linting setup.
Optimizing performance
Below are a few options you can use to improve your application build times.
Do not use AOT for local development. JIT should be faster for development purposes unless your browser would struggle with running JIT.
Consider using fork-ts-checker-webpack-plugin to parallelize TS type checking and code translation.
Not specifically related to Angular/TypeScript but also useful: https://webpack.js.org/guides/build-performance.
Finally, check this article on Devbridge blog which contains useful findings from other teams which improved development feedback look of their Angular application.
Reference materials
General
Architecture in Angular projects
More details on AngularJS LTS available here.
Hybrid application migration plan resources
Two Approaches to Upgrading Angular Apps
Alternate IFRAME Migration - Method
Devbridge blog
Resolving common technical debt to speed up Angular development