Updating your AngularJS App

(without the tears)
@ErinJZimmer
AngularJS vs Angular

You should upgrade your app

You can upgrade your app

@ErinJZimmer
  • Google developer expert
  • Mozilla tech speaker
  • Angular 1.4
  • Embedded in JSP
  • Built with Maven
  • Hopeless mess
- suffered from similar problems to many legacy enterprise apps - devs whose experience isn't in JavaScript - incredibly common

$scope.parentCtrl.loadDataForSection = function (section) {
  ...
}
					

resp.data instanceof Object && 
  typeof resp.data.oamErrorCode != "undefined"
					

Problems

  1. Security vulnerabilities
  2. Bad developer experience

Security Vulnerabilities

Brad to business - data breaches & $$
Jenkins plugin Says Java, .NET on the website Finds JS dependencies though
npm audit
npm v6
  • 6 high severity
  • 31 medium severity
  • 18 low severity
(just in JavaScript code) mostly XSS vulnerabilities & CSP bypasses, CORS requests able to run, HTML escaping tech lead presented to business about the risks of data breaches used a bunch of really scary numbers enough to make security our number one priority - NO MORE RELEASES UNTIL VULNS PATCHED! which was nice in theory

Improved Security

๐Ÿ’ฐ
how did we convince business?
  • Node/NPM
  • AngularJS
  • All dependencies
Node to 8/6, mostly so we could get the latest tools AngularJS to 1.7 dependencies to the latest we could - there were some limitations THE most difficult thing we did. Took several months.
APIs change.
Especially if you don't update for four years
<datepicker
  ng-model="objDate"
  starting-day="1"
  format-month="MMM"
  show-weeks="false"
  min-date="minDate"
  max-date="maxDate"
  data-datepickertype="inline">
</datepicker>
					
<div
  uib-datepicker 
  ng-model="$ctrl.date" 
  datepicker-options="$ctrl.opts">
</div>
					
0.13.3 of bootstrap => 2.5.6 much stuffing around

Good tests are essential

test behaviour not implementation need to know what your app is trying to achieve, not the steps it took to get there
it('should add a new phone number', () => {
 const phone = { type: 'MOBILE', number: '0412345678' }
 ctrl.addPhoneNumber(phone);

 expect(ctrl.updatePhoneTypeToSelect).toHaveBeenCalled();
 expect(ctrl.setPhoneNumberLabel).toHaveBeenCalled();
});

expect(ctrl.func1).toHaveBeenCalled();
expect(ctrl.func2).toHaveBeenCalled();
expect(ctrl.phoneTypes).toContain('MOBILE');
expect(phone.label).toBe('Mobile (2)');	 
- things are still going to break when the external APIs change but at least you'll know what your app was trying to achieve rather than just what it was doing

The bad news

Realistically, though, upgrading the dependencies of a very old app is going to be hard Take a lot of effort. Stuff will be missing/broken. Having good tests will help, but it's going to be hard to right them retroactively. Upgrading a library at a time _might_ help (we did everything at once), but often dependency chains are going to make that impossible

The good news

You don't really need to keep everything up to date You can stop this from happening to the next devs (or yourself in the future)

Break the build
for
security vulnerabilities

we do it using the dependency check plugin mentioned earlier but there's an even easier way

Terrible Developer Experience

Can't have a good app if we don't have good devs Can't get (and keep) good devs if they have to work on a pile of shit

var premiseLocation = null;
angular.forEach(this.accounts,
 angular.bind(this, function (location) {
  angular.forEach(
   location.accounts,
   angular.bind(this, function(account) {
    if (account.number == this.accountId) {
      premiseLocation = location;
    }
   })
  );
 })
);
					
code base is incredibly dated

var modules = [
 modulesPath + "jquery/dist/jquery.js",
 modulesPath + "autofill-event/autofill-event.js",
 modulesPath + "angular/angular.js",
 modulesPath + "bootstrap/dist/js/bootstrap.min.js",
 modulesPath + "d3/d3.min.js",
 modifiedModulesPath + "angular-payments/lib/angular-payments.min.js",
 modifiedModulesPath + "nvd3/build/nv.d3.js",
 modulesPath + "angular-animate/angular-animate.js",
 modulesPath + "angular-aria/angular-aria.js",
 modulesPath + "angular-ui-bootstrap/ui-bootstrap-tpls.js",
 modulesPath + "angular-cookies/angular-cookies.js",
 modulesPath + "angular-nvd3/dist/angular-nvd3.js",
 modulesPath + "angular-sanitize/angular-sanitize.js",
 modulesPath + "angular-shims-placeholder/dist/angular-shims-placeholder.js",
 modulesPath + "angular-touch/angular-touch.js",
 modulesPath + "angular-ui-router-breadcrumbs/dist/angular-ui-router-breadcrumbs.js",
 modulesPath + "angular-ui-router/release/angular-ui-router.js",
 modulesPath + "matchmedia-ng/matchmedia-ng.js",
 modulesPath + "matchmedia-polyfill/matchMedia.addListener.js",
 modulesPath + "matchmedia-polyfill/matchMedia.js",
 modulesPath + "moment/min/moment.min.js",
 modulesPath + "ng-csv/build/ng-csv.js",
 modulesPath + "ng-idle/angular-idle.js",
 modulesPath + "ng-joyride/ng-joyride.js",
 modulesPath + "spin.js/spin.js",
];
- manual gulp build instead of proper dependency management - single angular module, so it doesn't matter what order all the JS is imported - made testing in isolation hard, but nothing in the app was isolated anyway

app|develop โ‡’ ls components
_components.scss               card-mask
accessible                     checkbox                       is-desktop
accordion-panel                concession-card                is-logged-in      
account-balance                confirmation-modal             life-support      
account-heading                constants                      link              
account-holders                contact-details                live-chat         
account-payment-plan           creditcard-type                location-state    
account-premise-trim           currency                       logging           
account-select                 date-entry                     login             
account-type                   date-range-filter              login-form        
accounts                       device-detection               logout            
address-concat                 direct-debit                   maintenance       
address-input                  direct-debit-form              max-value         
admin                          direct-debit-form-rpo          mhbills           
agent                          double-click                   min-value         
anchor-smooth-scroll           eaexpiry-filter                movers            
authorization-http-interceptor error-http-interceptor         now               
autofill-event                 events                         oam               
bill-header                    federation                     omniture          
blank-string-filter            fibre                          password-reset    
bundle                         filtered-dropdown              payment           
button-with-icon               focus-input                    phone-number-input
calendar-countdown             form-submit                    phone-numbers     
capitalize                     forms                          plan-details      
carbon-neutral                 icon                           profile
					
Everything in global state ๐ŸŒ

Better Developer Experience

  • ๐Ÿ—๏ธMove to modern framework๐Ÿ—๏ธ
  • ๐Ÿ”ฅBurn the old codebase in a fire ๐Ÿ”ฅ
You should be looking at a path away from AngularJS altogether as it will go out of support 30/6/2021. which is not as far away as it sounds.
-- considered other frameworks -- obvious upgrade path -- lots of other apps in Angular, share resources -- devs not experienced in JS
ngUpgrade
-- allows you to run AngularJS and Angular code side-by-side -- diagram/description of how it works in longer talk maybe -- relatively straightforward, if you're coming from a modern codebase -- if you're doing it now, you're probably not coming from a modern codebase so there are some things you need to be aware of

Preparation

  • Follow the AngularJS Style Guide
  • Use a Module Loader
  • Migrate to TypeScript
  • Use Component Directives

Tidying Up

-- the most straightforward bit
  • Valid HTML
  • Strict dependency injection
<div class="section-icon-circle"/>
-- might seem obvious, but we didn't do this -- for some reason - heaps of self-closing div tags, not valid -- took me ages to work out what Angular was complaining about
  • Valid HTML
  • Strict dependency injection
Angular dependency injection requires that you pass the name of your dependencies

var myService = function ($http, $q) { 
	... 
}
					
var x=function(a,b){ ... }

var myService = ['$http', '$q',
 function ($http, $q) { ... } 
];
					
-- this turns out to be quite tedious, so some clever person came up with a way to do it automatically
ng-annotate
-- problem is, this tool is now deprecated, and there isn't an obvious replacement -- there is a webpack loader, but it doesn't work nicely with Angular CLI
find . -type f -iname '*.js' -exec ng-annotate -a {} -o {} \;

var myService = ['$http', '$q',
 function ($http, $q) { ... } 
];
					
-- this turns out to be quite tedious, so some clever person came up with a way to do it automatically

Switching to TypeScript

-- use tsc or Angular CLI -- CLI is less work overall, but has to be done kind of in one big chunk -- setup steps are the same either way

Setup ES Modules


var modules = [
 modulesPath + "jquery/dist/jquery.js",
 modulesPath + "autofill-event/autofill-event.js",
 modulesPath + "angular/angular.js",
 modulesPath + "bootstrap/dist/js/bootstrap.min.js",
 modulesPath + "d3/d3.min.js",
 modifiedModulesPath + "angular-payments/lib/angular-payments.min.js",
 modifiedModulesPath + "nvd3/build/nv.d3.js",
 modulesPath + "angular-animate/angular-animate.js",
 modulesPath + "angular-aria/angular-aria.js",
 modulesPath + "angular-ui-bootstrap/ui-bootstrap-tpls.js",
 modulesPath + "angular-cookies/angular-cookies.js",
 modulesPath + "angular-nvd3/dist/angular-nvd3.js",
 modulesPath + "angular-sanitize/angular-sanitize.js",
 modulesPath + "angular-shims-placeholder/dist/angular-shims-placeholder.js",
 modulesPath + "angular-touch/angular-touch.js",
 modulesPath + "angular-ui-router-breadcrumbs/dist/angular-ui-router-breadcrumbs.js",
 modulesPath + "angular-ui-router/release/angular-ui-router.js",
 modulesPath + "matchmedia-ng/matchmedia-ng.js",
 modulesPath + "matchmedia-polyfill/matchMedia.addListener.js",
 modulesPath + "matchmedia-polyfill/matchMedia.js",
 modulesPath + "moment/min/moment.min.js",
 modulesPath + "ng-csv/build/ng-csv.js",
 modulesPath + "ng-idle/angular-idle.js",
 modulesPath + "ng-joyride/ng-joyride.js",
 modulesPath + "spin.js/spin.js",
];
					
-- remember our gulpfile from before, that concatenated all our dependencies manually? -- not going to cut it with TypeScript - we want all this stuff managed automatically using ES modules -- good news: code doesn't need to be written as ES modules to be imported as ES modules

import 'angular';
import 'jquery';
import 'matchmedia-ng';
import 'angular-shims-placeholder';
import 'angular-animate';
import 'angular-sanitize';
import 'angular-touch';
import 'angular-aria';
import 'angular-cookies';
import 'ng-csv';
import 'ng-idle';
import 'ng-joyride';
					
-- use import 'module-name' syntax to import all your node module dependencies -- don't know of any easy way to automate this as node-modules aren't consistent in naming -- IDE might autocomplete for you -- hopefully you don't have too many dependencies... -- we were lucky - almost all our dependencies were dependencies of our single Angular module -- also need to import all your app files

import './components/carbon-neutral/carbon-neutral-service';
import './components/omniture/omniture-service';
import './components/max-value/max-value-directive';
import './components/login-form/login-form-directive';
import './components/form-submit/form-create-submit-directive';
import './components/form-submit/form-submit-directive';
import './components/show-back-button/show-back-button-service';
import './components/date-entry/date-entry-directive';
import './components/eaexpiry-filter/eaexpiry';
import './components/calendar-countdown/calendar-countdown-directive';
import './components/scroll-to-top/scroll-to-top-service';
					
-- exactly the same approach, except this time it is automatable, because you want to include all your JavaScript files (except test files, and you've used a sensible naming convention for those, of course)
find . -type f -iname '*.js' ! -iname '*_test.js'
> legacy-imports.js
find & replace regex or multicursor or whatever in IDE -- demo!

./components/carbon-neutral/carbon-neutral-service
./components/omniture/omniture-service
./components/max-value/max-value-directive
./components/login-form/login-form-directive
./components/form-submit/form-create-submit-directive
./components/form-submit/form-submit-directive
./components/show-back-button/show-back-button-service
./components/date-entry/date-entry-directive
./components/eaexpiry-filter/eaexpiry
./components/calendar-countdown/calendar-countdown-directive
./components/scroll-to-top/scroll-to-top-service
					
-- exactly the same approach, except this time it is automatable, because you want to include all your JavaScript files (except test files, and you've used a sensible naming convention for those, of course)

import './components/carbon-neutral/carbon-neutral-service';
import './components/omniture/omniture-service';
import './components/max-value/max-value-directive';
import './components/login-form/login-form-directive';
import './components/form-submit/form-create-submit-directive';
import './components/form-submit/form-submit-directive';
import './components/show-back-button/show-back-button-service';
import './components/date-entry/date-entry-directive';
import './components/eaexpiry-filter/eaexpiry';
import './components/calendar-countdown/calendar-countdown-directive';
import './components/scroll-to-top/scroll-to-top-service';
					
-- exactly the same approach, except this time it is automatable, because you want to include all your JavaScript files (except test files, and you've used a sensible naming convention for those, of course)

import 'angular';
import 'jquery';
import 'matchmedia-ng';
import 'angular-shims-placeholder';
import 'angular-animate';
import 'angular-sanitize';
import 'angular-touch';
import 'angular-aria';
import 'angular-cookies';
import 'ng-csv';
import 'ng-idle';
import 'ng-joyride';
						
import './components/carbon-neutral/carbon-neutral-service';
import './components/omniture/omniture-service';
import './components/max-value/max-value-directive';
import './components/login-form/login-form-directive';
import './components/form-submit/form-create-submit-directive';
import './components/form-submit/form-submit-directive';
import './components/show-back-button/show-back-button-service';
import './components/date-entry/date-entry-directive';
import './components/eaexpiry-filter/eaexpiry';
import './components/calendar-countdown/calendar-countdown-directive';
import './components/scroll-to-top/scroll-to-top-service';
					
-- exactly the same approach, except this time it is automatable, because you want to include all your JavaScript files (except test files, and you've used a sensible naming convention for those, of course)

Create App Entry Point

app.js
angular.module('selfService',
  [ 'ui-bootstrap', ... ]);

		import 'angular';
		import 'jquery';
		import 'matchmedia-ng';
		import 'angular-shims-placeholder';
		import 'angular-animate';
		import 'angular-sanitize';
		import 'angular-touch';
		import 'angular-aria';
		import 'angular-cookies';
		import 'ng-csv';
		import 'ng-idle';
		import 'ng-joyride';

		import './app.js';
								
		import './components/carbon-neutral/carbon-neutral-service';
		import './components/omniture/omniture-service';
		import './components/max-value/max-value-directive';
		import './components/login-form/login-form-directive';
		import './components/form-submit/form-create-submit-directive';
		import './components/form-submit/form-submit-directive';
		import './components/show-back-button/show-back-button-service';
		import './components/date-entry/date-entry-directive';
		import './components/eaexpiry-filter/eaexpiry';
		import './components/calendar-countdown/calendar-countdown-directive';
		import './components/scroll-to-top/scroll-to-top-service';

		export const legacyApp = angular.module('selfService');
							
-- exactly the same approach, except this time it is automatable, because you want to include all your JavaScript files (except test files, and you've used a sensible naming convention for those, of course)

Angular CLI

ng new myApp
-- in a normal setup, ng new and away - follow process from previous meet-ups -- our app is a Maven app
-- app-name
  -- package.json
  -- src
    -- main
      -- java
      -- webapp
        -- Angular app
    -- test
Angular CLI project structure
is configurable... kind of
"self-service-app": {
 "root": "",
 "sourceRoot": "self-service-app/src/main/webapp/src",
 "projectType": "application",
 "prefix": "app",
 "schematics": {},
 "targets": {
  "build": {
    "builder": "@angular-devkit/build-angular:browser",
    "options": {
      "outputPath": "self-service-app/src/main/webapp/dist/self-service-app",
      "index": "self-service-app/src/main/webapp/src/index.html",
      "main": "self-service-app/src/main/webapp/src/main.ts",
      "polyfills": "self-service-app/src/main/webapp/src/polyfills.ts",
      "tsConfig": "self-service-app/src/main/webapp/src/tsconfig.app.json",
      "assets": [
        "self-service-app/src/main/webapp/src/favicon.ico",
        "self-service-app/src/main/webapp/src/assets",
        "self-service-app/src/main/webapp/src/reference-data"
      ],
      "styles": ["self-service-app/src/main/webapp/src/styles.scss"],
					
angular.json can update it
ng generate component myNewComponent
At this point, you can just follow the standard Angular upgrade instructions.

upgrade as usual...

Writing Angular

check the documentation for how to actually do it
  • Angular component in AngularJS
  • AngularJS service in Angular
  • Angular service in AngularJS
  • AngularJS directive in Angular
angular.component('my-legacy-component', 
			function () {...})
  • restrict: 'E'
  • controller & template
  • isolate scope
  • bindToController
  • NO compile or link

The future

So we've moved to a new framework, how are we planning to burn the old codebase in a fire?

microfrontends

Hybrid app

Nx app

Hybrid app

Nx app

  • ๐Ÿ—๏ธMove to modern framework๐Ÿ—๏ธ
  • ๐Ÿ”ฅBurn the old codebase in a fire ๐Ÿ”ฅ
๐Ÿ’ฉ -> ๐ŸŽ‰๐Ÿ’ƒ๐Ÿ’ƒ๐Ÿ’ƒ๐ŸŽ‰

4 months

also developing new features at the same time

Lessons learned

  • Write good tests
  • A stitch in time...
  • There is hope!
even horrible apps can be fixed everything will be legacy one day - put in the extra effort now
You should be looking at a path away from AngularJS altogether as it will go out of support 30/6/2021. which is not as far away as it sounds.
ng-upgrade.ez.codes
@ErinJZimmer