Writing Angular 1.5 project in ES6/ES2015 [“NG 1.5 from the trenches” 5/7]

This post is the 5th part of the “Angular 1.5 from the trenches” series, presenting the architecture of a component-oriented, “NG 2 ready” Angular 1.5 app we’re building.
The module.component() method, introduced in Angular 1.5, may seem only a cosmetic addition, but the approach it promotes (and which we utilise to the full extent) results in a very different flavour of architecture than most of the “classic” Angular tutorials describe, so I hope you’ll learn a thing or two from our experience.

Table of Contents:

As you could notice if you read the previous posts in this series, we use ES6 in our project. It changes the way you write Angular 1.5 components (and other constructs like services or tests), so in this post, I’ll briefly discuss how the main NG 1.5 elements look like in ES6.

But first, a brief aside:

Why ES6?

There are two alternatives for writing NG 1.5 code: ES5 and TypeScript. So why ES6 and not one of them?

why not ES5:

This one is really a no-brainer. As you’ll see in a moment, ES6 results in a nicer Angular syntax and adds so many cool features to JS that the only reasonable argument for ES5 is to avoid transpilation. However, in a modern Angular project, you’ll have transpilation anyway (e.g. for minification, or CSS preprocessing, or caching Angular templates) so adding another small step to your build pipeline to transpile ES6 is not a big deal.

why not TypeScript:

As I mentioned in my first post in the series, most of our team was new to Angular, so we didn’t want to introduce too much new stuff at once. Also, TS is not as tightly integrated with NG 1.5 as it is with NG 2, and would’ve introduced additional overhead (configuring our build pipeline, installing typings etc.). It’s also easy to migrate to TS from ES6, so we’ve decided to start with ES6 and possibly switch to TypeScript in the future if our app becomes complex enough that the benefits of static typing will overweight the drawbacks of more complicated integration.

How to write Angular code in ES6?

Let’s go through all the constructs we use in NG 1.5 and see how ES6 impacts the way we write them:

Services

An Angular service maps nicely to a plain ES6 class.

export class SomeService {
  constructor(dependencyInjectedByAngularDI, anotherSuchDependency) {}

  someMethod() {...}

  get someGetter() {...}

  set someSetter(newValue) {...}
}

Dependencies, as you can see in the snippet above, are injected through the class constructor. The best way to register such a class with the Angular DI system is through a module().service provider, like this:

import { SomeService } from 'some.service';
angular.module('some.module').service('someService', SomeService);

Angular module().factory provider turned out to be almost obsolete when using ES6 (we use it only for some rare corner cases when we need to register a plain function with dependencies – which will be described below).

Components

Components in NG 1.5 consist of several elements: template, controller, and bindings, so they don’t map so nicely into ES6 classes as services – we still have to provide a “classic” configuration object for a component:

const SOME_CONST = 'SOME CONST';

export const SomeComponent = {
  bindings: {
    someBinding: '<'
  },
  controller: class {
    constructor(dependencyInjectedByAngularDI, anotherSuchDependency) {}

    someMethod() {...}

    get someGetter() {}
  },
  template: `
    <div>a multi-line template</div>
    <div>{{ $ctrl.someGetter }}</div>
    <div>${ SOME_CONST }</div>
  `
};

However, we can still make a component config object much nicer by using various ES6 features:

  • we use ES6 anonymous class for the component controller (with dependencies injected through the class constructor, the same as in the case of services)
  • we use new ES6 template strings for multi-line, inline templates
  • we use getters for a nicer looking templates (although getters were first introduced in ES5, I think they are still worth mentioning)
  • we use ES6 string interpolation to embed constants, results returned from plain helper functions, or to iterate over enums in inline templates, without having to pass them through the component controller

Filters

Filters are an Angular element the least impacted by using ES6 syntax. They still look almost the same as before (apart from the new, more terse arrow function syntax):

export function SomeFilter() {
  return (value) => { ... };
}

Plain helper functions and objects

Apart from components, services and filters we often use plain JS functions (and sometimes also objects) in our project. ES6 makes it really easy, especially in the simplest case:

simple functions and objects that do not require stubbing

In such a case we don't need Angular Dependency Injector / module system at all. We can just export our helpers using ES6 modules like this:

export function someHelperFunction(someParam) { ... };
export const SomeHelperObject = {
  someField: 'someValue',
  someMethod: (someParam) => { ... }
};

And then in our component import them – again via ES6 module system – like this:

import { someHelperFunction } from 'some-helper-function';
import { SomeHelperObject } from 'some-helper-object';

export const SomeComponent = {
  controller: class {
    someMethod() {
      let someValue = someHelperFunction(someParam);
      let anotherValue = SomeHelperObject.someMethod(someParam);
      ...
    }
  },
  template: ` ... `
};

This method is the most straightforward, but it has a drawback – our helpers are instantiated inside our component code and therefore can't be stubbed or mocked, which may be problematic in the case of more complex helpers. In such a case we can use a different approach:

complex functions and objects that we need to stub or mock

In such a case we don't import our helpers directly, but via Angular's module / DI system, as constants:

import { someHelperFunction } from 'some-helper-function';
import { SomeHelperObject } from 'some-helper-object';

angular.module('some.module')
  .constant('someHelperFunction', someHelperFunction)
  .constant('SomeHelperObject', SomeHelperObject);

Such constants can be then injected into our component, which makes it possible to stub them in tests:

export const SomeComponent = {
  controller: class {
    constructor(someHelperFunction, SomeHelperObject) {
      this.someHelperFunction = someHelperFunction;
      this.SomeHelperObject = SomeHelperObject;
    }

    someMethod() {
      let someValue = this.someHelperFunction(someParam);
      let anotherValue = this.SomeHelperObject.someMethod(someParam);
      ...
    }
  },
  template: ` ... `
};

The last and most complex case is when our helpers themselves have dependencies. This, again, requires a different approach.

helper functions and objects with dependencies

Helpers with dependencies are the only (relatively rare) case when we have to use Angular factories:

export function someHelperFunctionFactory(dependencyInjectedByAngularDI, anotherSuchDependency) {

  return (someParam) => { ... };
};
export function someHelperObjectFactory(dependencyInjectedByAngularDI, anotherSuchDependency) {

  return {
    someField: 'someValue',
    someMethod: (someParam) => { ... }
  };
};
import { someHelperFunctionFactory } from 'some-helper-function';
import { someHelperObjectFactory } from 'some-helper-object';

angular.module('some.module')
  .factory('someHelperFunction', someHelperFunctionFactory)
  .factory('SomeHelperObject', someHelperObjectFactory);

We then inject these factories into our component and use them in exactly the same way as in the case of helpers injected as constants:

export const SomeComponent = {
  controller: class {
    constructor(someHelperFunction, SomeHelperObject) {
      this.someHelperFunction = someHelperFunction;
      this.SomeHelperObject = SomeHelperObject;
    }

    someMethod() {
      let someValue = this.someHelperFunction(someParam);
      let anotherValue = this.SomeHelperObject.someMethod(someParam);
      ...
    }
  },
  template: ` ... `
};

Non-singleton classes

Another special case is non-singleton classes. Normally classes are a perfect use-case for Angular services, but NG services have one caveat – they are singletons. If we need a separate instance of a class for each component instance, we can't, unfortunately, use a service. In such a case we use a very similar set of techniques as for plain helper functions and objects:

simple classes that don't have to be stubbed

In such a case we just export / import a class via ES6 module system, bypassing Angular injector:

export class SomeHelperClass {
  constructor(someParam) { ... }

  someMethod() { ... }
}
import { SomeHelperClass } from 'some-helper-class';

export const SomeComponent = {
  controller: class {
    constructor() {
      this.helperInstance = new SomeHelperClass(someParam);
    }

    someMethod() {
      let someValue = this.helperInstance.someMethod();
      ...
    }
  },
  template: ` ... `
};

The only difference between a class and a function or object is that the class need to be instantiated by calling new. The best place for this is the component’s constructor, where we instantiate a class and assign the instance to a component field.

complex classes that have to be stubbed in tests

In this case, we’re forced to rely on Angular DI system. Again, in a similar vein to plain helper functions and objects, we inject such a class as a constant:

import { SomeHelperClass } from 'some-helper-class';

angular.module('some.module').constant('SomeHelperClass', SomeHelperClass);
export const SomeComponent = {
  controller: class {
    constructor(SomeHelperClass) {
      this.helperInstance = new SomeHelperClass(someParam);
    }

    someMethod() {
      let someValue = this.helperInstance.someMethod();
      ...
    }
  },
  template: ` ... `
};

classes with dependencies

If a non-singleton class has its own dependencies, we have to encapsulate it inside a factory function:

export function someHelperClassFactory(dependencyInjectedByAngularDI, anotherSuchDependency) {

  return class {
    constructor(someParam) { ... }
  };
};

Then, we can inject it as an Angular factory:

import { someHelperClassFactory } from 'some-helper-class';

angular.module('some.module').factory('SomeHelperClass', someHelperClassFactory);

And, as before, instantiate it in our component by manually calling new and assigning the class instance to a component’s field:

export const SomeComponent = {
  controller: class {
    constructor(SomeHelperClass) {
      this.helperInstance = new SomeHelperClass(someParam);
    }

    someMethod() {
      let someValue = this.helperInstance.someMethod();
      ...
    }
  },
  template: ` ... `
};

Unit Tests

The last area where ES6 impacts the way you write Angular code are Unit Tests.

The first and most noticeable thing is nicer, terser syntax enabled by ES6 arrow functions:

describe('SomeComponent', () => {
  beforeEach(() => {
    ...
  });

  it('does something cool', () => {
    ...
  });
});

The second thing is the way you can test services.

The "classic" way of testing a service is to obtain an instance from Angular injector:

describe('SomeService', () => {

  let service;

  beforeEach(angular.mock.module('some.module', ($provide) => {
    $provide.value('someDependency', mockedDependency);
    $provide.value('anotherDependency', anotherMockedDependency);
  }));

  beforeEach(angular.mock.inject((someService) => {
    service = someService;
  }));

  it('does something cool', () => {
    expect(service).toDoSomethingCool();
  });
});

This requires a bit of scaffolding: using Angular’s mock.module, providing mocks of the dependencies of tested service through NG’s DI system (using $provide), and using NG’s mock.inject to obtain the instance.

With ES6 modules and services as ES6 classes, the scaffolding becomes simpler – we can just import our service and manually create instance via new keyword, providing all the mocked dependencies in the constructor:

import { SomeService } from 'some.service';

describe('SomeService', () => {

  let service;

  beforeEach(() => {
    service = new SomeService(mockedDependency, anotherMockedDependency);
  });

  it('does something cool', () => {
    expect(service).toDoSomethingCool();
  });
});

The above test doesn’t even require Angular at all.

The drawback of this approach is that you still have to use the standard Angular test scaffolding for components and for some of the services with more complex dependencies (e.g. if you want to use httpBackend for mocking AJAX requests). It’s your call if you prefer maximum simplicity or maximum consistency of your tests (in our team we tend to lean towards consistency).

(more about testing NG 1.5 components in the next post)

Conclusion

ES6 makes Angular 1.5 code nice, clean and terse. It should be a default choice for starting NG 1.5 project.

Over to you

Which Javascript dialect do you use in your Angular project? What do you think about using ES6 vs TypeScript? I’d love to hear from you!

Subscribe for the rest of the “NG 1.5 from the trenches” series

If you want to get notified when the next posts in the series get published, subscribe to my blog via RSS, follow me on Twitter or subscribe below via mail:

Enter your email address to follow this blog and receive notifications of new posts by email.

One thought on “Writing Angular 1.5 project in ES6/ES2015 [“NG 1.5 from the trenches” 5/7]

  1. This is by far the most interesting article I’ve found about real ES6 with angular 1. Thanks for sharing, really appreciated!

    Like

What do you think?

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s