Image by MattysFlicks — CC BY-NC-ND 2.0

Upgrading an Angular ES5 codebase to ES6

Dependency Injection is a blessing and a curse…

Back when we were all writing AngularJS ES5 code, there was usually an app.js that listed all of your libraries and dependencies in a single module. All your other files simply added .controller() or .service() to that global module and everything worked.

If you were thinking about reusability (and looking at libraries like angular-strap or ui-bootstrap) you might even create more modules for the features in your application:

src
- module
- app -> module('app', ['common', 'feature1', ...]
- common -> module('app.common', [...])
- feature1
- feature2 -> module('app.feature2', ['sub-feature'...]
- sub-feature2-1
- ...

Looks reasonable. Uses modules. Should be easy to port to ES6. Right?

Modular code

Code is not modular just because it is inside an angular.module(). Modular code:

  • has explicit dependencies. This allows you to know exactly what other-code the module needs for it to work. Every Angular module should indicate that it depends on Angular, for example.
  • is portable — you can move it another project, or folder, and it shouldn’t break. If it does break, it should only be due to a dependency path being wrong (if the module depended on ../otherModule for example)
  • has low coupling. It only depends on code that it needs — no more, no less.
  • … has other qualities too (e.g. good abstractions, functional cohesion), but the main ones I want to talk about in this article are the ones above.

ES6 code provides the import and export statements for defining dependent modules and exporting modules. Upgrading your project to use ES6 provides you with the tools to be explicit about your dependencies in a way that wasn’t possible in ES5. Yay!!!

But the downside comes when you decide to use a tool like Webpack to build your application (which you should, for any web-app larger than a demo, IMHO).

Webpack & Modules

Webpack is a module bundler. Starting with your source code, it detects the dependencies between all of your files and can create a set of bundles (one-or-more files).

This solves one of the headaches of ES5 website development — defining your <script> elements in the correct order (or rather, defining your dependencies in the correct order). Webpack does this dependency-analysis for you!

For Webpack to do this correctly, you must be explicit about the dependencies of each and every module. But your Es5 code doesn’t contain any import or export statements!?!

That moment when you realise how much work is ahead of you…

Step 1 — Defining your dependencies

I’ll give you 1–3 weeks to go ahead and refactor your application to add the missing dependencies to each module and export each module. A standard AngularJS ES6 module pattern that you can use — and that I’ve found that scales well — looks like this:

// Define imports
import angular from 'angular'; //usually works
// Depending on Babel & plugins, you may need:
// import * as angular from 'angular';
import entityName from './localModule'; // this is the module name
// Define Angular module using Java-like namespace
const mod = angular.module('company.app.feature', [
entityName // <--- I don't need to know what the module name is :)
]);
// Add component(), service(), etc to the module
mod.component('someComponent', () => {
...
})
mod.service('someService', () => {
...
})
export default mod.name;  // MUST export the module name

Great! Now that you’ve done that, your application should work with Webpack. But what about your tests?

Testing with ES6, Webpack and Karma

In Es5, Jasmine+Karma tests typically worked by having <script> tags for all of your dependent libraries included in the test HTML page, so that your test itself looked something like this:

// ES5 example
describe(‘My component’, function() {
beforeEach(function() {
// Include the module that has the unit you are testing
angular.mock.module(‘app’); // the whole app!!!
});

it(‘should do something’, ...);
});

In the above example, the ‘app’ module is being included because it contains the component you want to test. If ‘app’ has run() or config() blocks, your test could be affected by the side-effects produced within those functions. Modules that contain lots of things make it difficult to test the single-thing you are trying to test in isolation (free of side effects).

When you use Karma with Webpack, you can use the much better approach of telling Karma to only include your unit test specifications (as it’s entry point). The test specs will import the ‘unit’ you are testing, which will import it’s dependencies too.

comp.spec.js -> comp.js -> angular, utils.js, ...

What this means is that each test spec is able to test your component/service/… in isolation — exactly what you want. Webpack+Karma only load the parts of your code that you are actually testing, which also makes your tests run faster (after the initial compilation).

Step 2 — Re-define your dependencies correctly

So here’s what your ES6 test should look like:

import moduleIWantToTest from '../component.js'; // this is a string
describe('My component', () => {
beforeEach(() => {
angular.mock.module(moduleIWantToTest); // nice!
});

it('should do something', ...);
});

…but then your test fails because the injector can’t find something, or you component doesn’t render the child components correctly.

In my experience, it is when you’re trying to unit test your components that you discover their actual dependencies — the ones we don’t think about but are actually there.

For example, if you are testing a routing component that uses ui-router, you’ve probably included ui-router as a dependency in some top-level module. But what about the component that actually uses the $state service — did you remember to import uiRouter from ‘ui-router’ and then write const mod = angular.module(‘app.routing’, [uiRouter]);? I know I didn’t when I first started refactoring code like this.

How about your visual component which has a HTML template like this:

<div class="amazing-style">
<my-custom-list-view data="$ctrl.yada"/>
</div>

Does your component’s module definition have a dependency on the module-that-contains the myCustomListView component (e.g. const mod = angular.module(‘myComponent’, [myCustomListViewModule])?

The good news is that unit testing will reveal all these previously-undefined dependencies! It can just take a bunch of time to go through each test and “rediscover” the missing dependencies.

Step 3 — Use Webpack for all your other dependencies

Source code: check. Unit tests: check. HTML? CSS? Well that’s the cool thing about Webpack — it can treat all kinds of code as ‘just another module’ and bundle things for you.

In the Angular world, a common way to include HTML templates looks like this:

mod.component('myComponent', {
controller: () => { /* do something */ },
templateUrl: 'path/to/some/static.html',
});

…and you might have your own build-tooling which can find all your HTML files and put them into Angular’s $templateCache. But you could let Webpack do that for you:

mod.component('myComponent', {
controller: () => { /* do something */ },
template: require('./path/to/some/static.html')
});

Webpack — using the HTML Loader — will include the HTML into your JavaScript bundle as a string, and optionally minify it for you. How nice!

Similarly, the CSS Loader (with additional loaders for CSS pre-compilers like Stylus, SASS, Less and PostCSS) can bundle your CSS code as well. And if you don’t want it included inside your JS bundle, you can tell Webpack to output it as it’s own file instead. Sweet :)


Why is it so hard?

I think the reason why it is hard to convert AngularJS projects from ES5 to ES6 is… dependency injection.

Dependency injection in AngularJS is a great feature. It makes things easier to test. It ensures dependencies are available when the code executes. But it also means that your ES5 modules never need to be explicit about what their direct dependencies are; so long as some module somewhere loads the dependency, your module will receive the dependency and everything just works.

Now that ES6 allows us to describe our dependencies explicitly, we can avoid this problem entirely for future ES6 projects.

Summary

Converting an ES5 AngularJS codebase to use ES6 & Webpack will take some time but will mean you can get rid of 99% of your old build tools (Grunt, Gulp, Broccoli — whatever you were using). And you will finish with modern tooling, and with code that is both portable and modular.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.