Awesome Angular Controllers with ES6: 6 Easy Steps!

Anyone can write code that a computer can understand, it’s much more difficult to write code that other people can understand! Our team is always striving to make our code as easy to understand and intention revealing as possible. Using the new ES6 syntax and features can be a great way to improve your existing Angular application by making it more understandable (and easier to test).

Up until now we have been developing our Angular JS 1.x application using boring old ES5. In an effort to make our application code easier to read and understand, we recently began switching over to some of the new ES6 features. This can be accomplished today (before ES6 is fully supported by all the browsers) by using a transpiler. My blog post from last month explains how to add Babel to your build pipeline as a transpiler. Once you have this build pipeline setup, it’s pretty easy to gradually begin incorporating ES6 features into your application.

In this article we’re going to take a typical controller / html template and convert it to an ES6 class with other ES6 features like string templates, constructors, and arrow functions. The app is basically a simple to-do application with a super-hero twist. It allows you to add heroes along with their super power to a list and then modify the list(clear, remove items, and print to console). If you want to cut straight to the code samples, you can find them in the ngAndES6Examples repository.

I intentionally created a very simple example JS app framework (without a full build pipeline) to make it easier to follow and focus on the core ES6 features. For a real world application you will want to setup a full build pipeline (minification, bundling, linting, etc).

John Papa has a great AngularJS opinionated style guide that goes over some recommended best practices for developing Angular applications that make them easier to understand and maintain using standard ES5. These recommendations make your Angular code cleaner and they help you prepare your application to take advantage of ES6 features. The first four steps of this article actually just to apply these best practices to your code to make it easier to to transform to an ES6 and to take advantage of other ES6 features.

1. Initial ES6 Controller
Here is the initial controller for our example:

var module = angular.module('nges.ExampleController', []);

module.controller('ExampleController', function($scope) {

$scope.model = {
name: "",
power: ""
};

$scope.justforthedot = {
heros: []
};

$scope.reset = function() {

$scope.justforthedot.heros = [];
};

$scope.writeToConsole = function () {

$scope.justforthedot.heros.forEach(function(hero) { console.log(hero.name + ' is a super hero with the power of ' + hero.power); })
};

$scope.add = function(hero) {

$scope.justforthedot.heros.push(hero);
$scope.model = {
name: "",
power: ""
}
};

$scope.delete = function(index) {

$scope.justforthedot.heros.splice(index, 1);
}
});

Working in an Angular it’s common to see this kind of code. It works correctly, but it’s not extremely easy to read or understand. For a simple application like this it might not seem to make a huge difference. However on a more complex project with more logic and controllers, it can lead to a really messy code base (which usually means more flaws and greater potential for future flaws).

Here are some of the disadvantage of this type of code that we will attempt to remedy with our refactoring:

  • Use of $scope – Heavy reliance on the $scope object as an intermediary between the controller and the view. This is an unnecessary abstraction that makes things more difficult to read / understand as well as making it much more difficult to test.

  • Prototypical inheritance issues – Since we are dealing with $scope directly, you should never bind individual properties directly to the scope. Recommended best practice (to avoid unwanted behavior during prototypical inheritance) is to ensure that there is always a ‘.’ in a property reference. To this end you will see that we have added a .justforthedot object to ensure that the list of heros complies with this rule. Obviously this doesn’t make the code easier to follow.

  • String concatenation – Working with strings in ES5 leaves much to be desired. String concatenation is clumsy and can be extremely error prone. You’ll notice the simple string concatenation function in the writeToConsole() function. This is improved later through the use of the new ES6 string templating feature.

  • Global Scope – You’ll notice our main module is declare on the global scope.

2. Setter and Getters (Remove the unnecessary variable)
If you’re following the rule that says every Angular module should have its own code file, then using a variable to reference a module is superfluous. It also can lead to global variables, which is obviously bad. Here we remove that variable:

angular.module('nges.ExampleController', [])
.controller('ExampleController', function($scope) {

$scope.model = {
name: "",
power: ""
};

$scope.justforthedot = {
heros: []
};

$scope.reset = function() {

$scope.justforthedot.heros = [];
};

$scope.writeToConsole = function () {

$scope.justforthedot.heros.forEach(function(hero) { console.log(hero.name + ' is a super hero with the power of ' + hero.power); })
};

$scope.add = function(hero) {

$scope.justforthedot.heros.push(hero);
$scope.model = {
name: "",
power: ""
}
};

$scope.delete = function(index) {

$scope.justforthedot.heros.splice(index, 1);
}
});

3. Use a Separate Function
Removing the inline function makes your code easier to read and prepares you for turning from a constructor function into an ES6 class.

function ExampleController($scope) {

$scope.model = {
name: "",
power: ""
};

$scope.justforthedot = {
heros: []
};

$scope.reset = function() {

$scope.justforthedot.heros = [];
};

$scope.writeToConsole = function () {

$scope.justforthedot.heros.forEach(function(hero) { console.log(hero.name + ' is a super hero with the power of ' + hero.power); })
};

$scope.add = function(hero) {

$scope.justforthedot.heros.push(hero);
$scope.model = {
name: "",
power: ""
}
};

$scope.delete = function(index) {

$scope.justforthedot.heros.splice(index, 1);
}
}

angular.module('nges.ExampleController', [])
.controller('ExampleController', ExampleController);

4. Controller As
Using Angular’s new controller as syntax removes reliance on $scope, making it easier to read and test. Controller as binds your template directly to the properties and methods of the controller using this. Another great benefit is that since there is always a ‘.’ in your reference to properties (because of the controller name), it removes the need for the extra ‘.’ notation to address prototypical inheritance issues. This makes our controller a lot easier to read (not to mention to test!):

function ExampleController() {

this.model = {
name: "",
power: ""
};

this.heros= [];

this.reset = function() {

this.heros = [];
};

this.writeToConsole = function () {

this.heros.forEach(function(hero) { console.log(hero.name + ' is a super hero with the power of ' + hero.power); })
};

this.add = function(hero) {

this.heros.push(hero);
this.model = {
name: "",
power: ""
}
};

this.delete = function(index) {

this.heros.splice(index, 1);
}
}

angular.module('nges.ExampleController', [])
.controller('ExampleController', ExampleController);

You’ll notice that all bindings in the template are now reference by ctrl (as in this sample):

<div class="form-group">Super Power:
<div class="col-sm-2">
<div class="input-group">

<span class="input-group-btn">
Add
</span>

</div>
</div>
</div>

The name ‘ctrl’ is determined by this statement in your route definition or controller binding statement:

$routeProvider
.when('/', {
templateUrl: 'app/controllers/4-ControllerAs/example.tpl.html',
controller: 'ExampleController as ctrl'
});

Important safety tip!! Don’t reference your controller from the routing definition and your template. Your controller will load twice and along with it two different scopes!! It’s an easy mistake to make and can take a little while to troubleshoot.

You can choose the noun you like best! I chose controller, because that is actually what is being referenced. You could give it a specific name or use the same one in all of your controllers. Some people like to call it the view or viewModel.

5. ES6 Class Notation
Now we’re getting somewhere! This is where we leave the Angular best practices and make our way into using ES6 features.

Although we only covered a handful of them to get us towards ES6 classes, there are lots more fantastic recommendations in John Papa’s Angular Style Guide. I highly recommend reading over all of them. Also, remember that we did not go over how to setup the build pipeline for this using Babel, but if you’re interested I have another blog post on that topic. Once you have the build pipeline in place and a good solid ES6 controller, you’re ready to add ES6 features!

First we change our controller function to a class (which really gets transpiled to a constructor anyway):

class ExampleController {

constructor() {

this.model = {
name: "",
power: ""
};

this.heros= [];
}

reset() {

this.heros = [];
};

writeToConsole() {

this.heros.forEach(function(hero) { console.log(hero.name + ' is a super hero with the power of ' + hero.power); })
};

add(hero) {

this.heros.push(hero);
this.model = {
name: "",
power: ""
}
};

removeHero(index) {

this.heros.splice(index, 1);
}
}

angular.module('nges.ExampleController', [])
.controller('ExampleController', ExampleController);

There are some important changes to notice here, so let’s take a little more time reviewing this step:

  1. Class – We switched from a full function declaration for our controller to a class. For those used to working in more object oriented languages this probably seems very natural.

  2. Constructor – We initialized our controller with an ES6 constructor. Notice how this encourages us to put all of our bindable property declarations at the top of the class. This is another good design practice for Angular controllers that is made easier with ES6. This really helps to make the controller easier to understand.

  3. Removed this – The this keyword was removed from the declaration of all of our exposed class functions. It’s not necessary with an ES6 class and it was really superfluous anyway.

  4. Removed function keyword – ES6 class method declarations do not have the function() keyword. Nice! Less to read and it didn’t lend much value to understanding the code. +1

6. More ES6 Enhancements
Switch the inline, concatenated string to the new ES6 string template and use the new ES6 arrow syntax:

writeToConsole() {

this.heros.forEach((hero) => { console.log(`${hero.name} is a super hero with the power of ${hero.power}`); })
};

That’s so much more elegant and easy to read!
Sit back and enjoy a much cleaner controller!

For full examples of everything in this post, see the git repository ngAndES6Examples repository. Feel free to fork, comment, or make pull requests if you have improvements.

One thought on “Awesome Angular Controllers with ES6: 6 Easy Steps!

Leave a Reply

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