I already mentioned in the last post that at OpenProject, we have been migrating our AngularJS 1.6 frontend to Angular 5 (now 6). This has been an ongoing effort over the past 6 months and has concluded in the past weeks.
This blog posts reflects on my experiences on the continuous ugprade process while maintaining a usable development branch that neither blocks integration testing nor development on other areas of the application.
OpenProject consists of an AngularJS frontend powering our Work Package module for issues, tasks, and project management. It is one of the core components of the application and the main use of AngularJS in an otherwise mostly Rails monolithic application.
Adapting to change: Hierarchical re-use of existing AngularJS services
Due to a requirement for the release of 8.0. (released September 2018), where we wanted to reuse the work package list view in other contexts, a complex table view with filters, sorting, grouping and an integrated timeline. One part was integrating such a list for displaying subtasks. That required a hierarchical reinstantiation of state management services of the current list since those were global at the time.
We decided that the hierarchical DI of Angular would be a perfect fit for this use case and since we wanted to move to Angular at some unspecified point in time, that would be a good excuse to move the upgrade forward. This was only possible because Angular has a set of helpers known as ngUpgrade
to start upgrading an application step-by-step without replacing the entire frontend.
A gust of ngUpgrade and ui-router hybrid
For someone with AngularJS experience, Angular provides clear and truly extensive documentation regarding the upgrade path from AngularJS to Angular without the need to read any other parts of their documentation first. It would be useless to replicate parts of the excellent upgrade guide here. If you are interested in migrating a serious application from AngularJS to Angular, just work through the following article: https://angular.io/guide/upgrade. If you find yourself wanting to dig deeper into Angular concepts while you're migrating, all sections of the migration guide link to their documentation counterparts with more information.
We were intrigued to be able to run both applications in parallel during the development phase of 8.0. To ensure working environments, integration tests, and to avoid having a massive effort of conversion. The same goes for our usage of ui-router 1.x for AngularJS, which includes an upgrade module ui-router/angular-hybrid to route to AngularJS and Angular routes simultaneously. This comes at the cost of some restrictions that we will see below.
Caveats with ui-router
Our work package module routes are a tree. We have a single entry route that shares common parameters to all descending states. This is where the limitation of angular-hybrid comes into play very early in your upgrade path consideration:
Quoted from: https://github.com/ui-router/angular-hybrid#limitations
We currently support routing either Angular (2+) or AngularJS (1.x) components into an AngularJS (1.x)
ui-view
. However, we do not support routing AngularJS (1.x) components into an Angular (2+)ui-view
.If you create an Angular (2+)
ui-view
, then any nestedui-view
must also be Angular (2+).Because of this, apps should be migrated starting from leaf states/views and work up towards the root state/view.
I suggest you make a draft of the router states you want to convert to, e.g., Angular components and figure out what other states you will need to convert early on.
Caveats with ngUpgrade
When using ngUpgrade
to bootstrap a hybrid application, you can make Angular and AngularJS parts of the application intertwine. In your DOM, you will likely have a bootstrapped AngularJS body and thus entry directives will remain AngularJS. This is especially important in our Rails-backed application, where entrypoints into the AngularJS frontend are rendered dynamically on Rails.
The helpers from ngUpgrade
allow you to both downgrade Angular components for use within AngularJS templates and upgrade AngularJS directives for use within Angular. This generally works very well when paying attention to wiring the in- and outputs of converted directives. However, there is one important caveat: you can only downgrade components
, not attribute directives to AngularJS.
In our case, we had a lot of attribute directives that enhanced Rails-rendered elements such as forms with additional processing. These attribute directives are hard to convert, since Angular root components must not be attribute directives.
Mixing Rails with Angular
We were getting used to being able to a bootstrapped AngularJS body where we could arbitrarily render AngularJS directives (often attribute directives), to improve behavior on some elements such as forms or mixing Rails-rendered templates with directives - often to get the advantage of being able to develop component-like Rails views with some TypeScript added.
Also, entry components are expected to be empty — all content will be destroyed. If you were used to rendering transcluded content from Rails into directives, for example, this will no longer work when you're fully migrated to Angular.
Initial approach: Convert a component deep down in the module tree
To drive a wedge between the previous AngularJS and the hybrid upgrade frontend, we decided to convert a pretty isolated component in our work package module: The timeline rendering section. It is rendered with lots of manual statements and is deep down to avoid the ui-router caveat above.
TypeScript
That one was easy for us. We already converted almost our entire AngularJS frontend to TypeScript over two years ago. If you work with multiple people on a complex application, you cannot afford to avoid it. Get over the initial learning curve (which is pretty non-existant until you enforce strictness options) and enjoy making less mistakes.
Build system: Plain Webpack or Angular CLI?
From our experiences, we would suggest either to move to Angular CLI directly if you can afford it. We had a custom heavy Webpack configuration that forced us to stick with it and the tested ts-loader
as with AngularJS while using JIT and moving to CLI in a separate effort.
By sticking to your current build system, integrating ngUpgrade
will be much easier and you can focus on creating new components in Angular while converting existing views step-by-step. However, with the @ngtools/webpack
plugin, we were not able to configure AOT properly on Angular 6.0.
With some pain in the module definition and throughout the app, we have then migrated to Angular CLI and I do not regret it. If you're interested in the gory details and are not put off by huge merges, check out this pull request on our core.
Conclusion
We managed to upgrade a complex AngularJS application during development to Angular 6 (with phases in Angular 4 and 5). All this while maintaining testability and (more or less) overall functioning of the application while migrating an increasing set of components to Angular through regular pull requests.
This does not come without costs. We were restricted which components we could convert first due to restrictions in both ngUpgrade and ui-router hybrid, resulting in a large single pull request to get the rests driven out in one go.