ES6 Classes and Angular

Evan Williams
3 min readFeb 1, 2017

With the introduction of ES6 classes, certain Angular constructs became much more legible. In this post, I’ll highlight where to use them, convert a function based controller to an ES6 Class to show the clarity, and talk about when not to use them.

Before reading, please refer to my post about ES6 classes and when to use them.

Instantiated Objects

There are a few types of Angular constructs that are instantiated during the Angular lifecycle by calling new on the argument representing the construct. These include controller and service. When Angular creates these objects for use by the $injector, it instantiates the objects. Services will be instantiated once during the Angular lifecycle (when it is first needed by any construct). The reference returned is injected into each construct from there on out. This makes it a perfect way to pass data throughout the application.

Controllers are instantiated per use (for components, directive, routes, etc). These maintain the state of those individual uses.

Some examples of non-instantiated Angular constructs are filters, directives, and factories. These are simply called as functions and do not serve well as classes. It can be worked around by providing an anonymous function that news up an instance of the class. One would have to pass all of the injected arguments from the anonymous function to the class, as well, making maintenance a pain!

Benefits of an Angular Service/Controller as a Class

My main reason for using classes is the readability. Function notation is fine once developers are used to Javascript and understand the problems using the thiskeyword can get lead to in a codebase. However, it leads to what I consider some poor practices.

A developer is less likely to declare things in a sensible way. When everything is being assigned to the this variable (functions and values), it tends to intermix values and function declarations to where it is not easy to know where something is being defined. This becomes especially true as the function expands.

With classes, values cannot be defined outside of the functions.

In addition, it also highlights the functions on a particular class because of the notation.

Downsides

There’s only one downside that I complain about. In a constructor, the parameters passed have to be explicitly assigned onto the instance and referred to in other functions by this.parameter. I don't mind the behavior if developers maintain the injected name (a.k.a. MyService is assigned to the controller instance by setting this.MyService = MyService.

Typescript handles this beautifully by automatically assigning parameters to the class instance if defined as public/private etc, but that’s a different story.

An Example

function MyController($filter, MyService) {  
this.format = 'short';
this.updateDatabase = (viewData) => {
MyService.updateDatabase(viewData);
};
this.$onInit = () => {
this.dateValue = $filter.get('date')(this.date, this.format);
});
}

The above relies on the developer’s understanding of arrow functions. If the developer chose to use function, then they'd have to reference the controller instance by an aliased reference to this.

The above is also a significantly simplified example….but here it is as a class:

class MyController {  
constructor($filter, MyService) {
const format = 'short';
Object.assign(this, {
$filter, MyService, format
});
}
$onInit() {
this.dateValue = this.$filter.get('date')(this.date, this.format);
}
updateDatabase(viewData) {
this.MyService.updateDatabase(viewData);
}
}

There’s some extra boiler plate associated with the constructor and calling dependencies that are provided in the constructor. However, there is some benefit to the latter. That is, it’s easy to reference where the value came from. It was assigned somewhere onto the controller instance, which can be traced back to the constructor.

Some Best Practices when using Classes

  • Constructors are for setting constants and injected objects onto the controller, nothing else. This ensures proper initialization of values from bindings and services and the like.
  • Utilize $onInit for controllers for any additional set up necessary values that are calculated (based on bindings, values passed to the constructor, etc)
  • Hoist the Angular lifecycle hooks to the top. These include $onInit, $onChanges, $onDestroy, and $postLink. This will highlight the behavior of the controller specific to Angular. In addition, it pushes helper functions and view interactions to the bottom
  • Use Object.assign to clean up constructor notation utilizing shorthand object definition (where the property & variable name are the same).

Conclusion

In my 3 years of Angular experience, I’ve come to love the class notation where appropriate within an application. It cuts down on some of the verbosity of declaring functions, and also obfuscates the behavior of the this keyword, which is helpful to less experienced devs.

--

--