Writing AngularJS Apps Using ES6

Share this article

As many of you are aware, ECMAScript 6 is in its draft state now and is expected to be finalized some time this year. But it has already caught a lot of attention in the community and browsers have already started implementing it. We also have a number of transpilers like Traceur, 6to5, and many others that convert ES6 code to ES5 compatible code. Community members have started playing around with ES6 and many of them are blogging about what they learn. SitePoint’s JavaScript channel also has a good number of articles describing the different features of ES6. It is possible to write any piece of everyday JavaScript using ES6. To do this, we need to be aware of the key features of ES6 and know which piece fits where. In this article, we will see how we can use features of ES6 to build different pieces of an AngularJS application and load them using ES6 modules. We will do this by building a simple online book shelf application and we will see how it is structured and written. As ever, code for this application can be found on our GitHub repository.

A Note on the Bookshelf Application

The sample BookShelf application contains following views:
  1. Home page: Shows a list of active books. Books can be marked as read and moved to the archive from this page
  2. Add book page: Adds a new book to the shelf by accepting the title of the book and name of the author. It doesn’t allow a duplicate title
  3. Archive page: Lists all archived books

Setting up the Application for ES6

As we will be using ES6 to write the front-end part of the application, we need a transpiler to make the ES6 features understandable for all the browsers. We will be using the Traceur client-side library to compile our ES6 script on the fly and run it in the browser. This library is available on bower. The sample code has an entry for this library in bower.json. On the home page of the application, we need to add a reference to this library and the following script:
traceur.options.experimental = true;
new traceur.WebPageTranscoder(document.location.href).run();
The app’s JavaScript code is divided into multiple files. These files are loaded into the main file using the ES6 module loader. As today’s browsers can’t understand ES6 modules, Traceur polyfills this feature for us. In the sample code, the bootstrap.js file is responsible for loading the main AngularJS module and manually bootstraping the Angular app. We cannot use ng-app to bootstrap the application as the modules are loaded asynchronously. This is the code contained in that file:
import { default as bookShelfModule} from './ES6/bookShelf.main';
angular.bootstrap(document, [bookShelfModule]);
Here, bookShelfModule is name of the AngularJS module containing all the pieces. We will see the content of the bookShelf.main.js file later. The bootstrap.js file is loaded in the index.html file using the following script tag:
<script type="module" src="ES6/bootstrap.js"></script>

Defining Controllers

AngularJS controllers can be defined in two ways:
  1. Controllers using $scope
  2. Using the controller as syntax
The second approach fits better with ES6, as we can define a class and register it as a controller. The properties associated with an instance of the class will be visible through the controller’s alias. In addition, the controller as syntax is comparatively less coupled with $scope. If you are not aware, $scope will be removed from the framework in Angular 2, so we can train our brains to be less dependent on $scope from now on by using the controller as
syntax. Though classes in ES6 keep us away from the difficulty of dealing with prototypes, they don’t support a direct way of creating private fields. There are some indirect ways to create private fields in ES6. One of them is to store the values using variables at module level and not including them in the export object. We will use a WeakMap to store the private fields. The Reason behind choosing WeakMap is that those entries that have objects as keys are removed once the object is garbage collected. As stated above, the home page of the application loads and displays a list of active books. It depends on a service to fetch data and to mark a book as read, or to move it to the archive. We will create this service in the next section. So that the dependencies injected into controller’s constructor are available in instance methods, we need to store them in the WeakMaps. The home page’s controller has two dependencies: the service performing the Ajax operations and $timeout (used to show success messages and hide them after a certain time). We also need a private init method to fetch all active books as soon as the controller loads. So, we need three WeakMaps. Let’s declare the WeakMaps as constants to prevent any accidental re-assignment. The following snippet creates these WeakMaps and the class HomeController:
const INIT = new WeakMap();
const SERVICE = new WeakMap();
const TIMEOUT = new WeakMap();

class HomeController{
  constructor($timeout, bookShelfSvc){
    SERVICE.set(this, bookShelfSvc);
    TIMEOUT.set(this, $timeout);
    INIT.set(this, () => {
      SERVICE.get(this).getActiveBooks().then(books => {
        this.books = books;
      });
    });

    INIT.get(this)();
  }

  markBookAsRead(bookId, isBookRead){
    return SERVICE.get(this).markBookRead(bookId, isBookRead)
      .then(() => {
        INIT.get(this)();
        this.readSuccess = true;
        this.readSuccessMessage = isBookRead ? "Book marked as read." : "Book marked as unread.";
        TIMEOUT.get(this)(() => {
          this.readSuccess = false;
        }, 2500);
      });
  }

  addToArchive(bookId){
    return SERVICE.get(this).addToArchive(bookId)
      .then(() => {
        INIT.get(this)();
        this.archiveSuccess = true;
        TIMEOUT.get(this)(() => {
          this.archiveSuccess = false;
        }, 2500);
      });
  }
}
The above snippet uses following ES6 features:
  1. Classes and WeakMaps, as already mentioned
  2. The arrow function syntax to register callbacks. The this reference inside the arrow functions is same as the this reference outside, which is the current instance of the class
  3. The new syntax for creating a method and attaching it to an object without using the function keyword
Let’s apply dependency injection and register this class as a controller:
HomeController.$inject = ['$timeout', 'bookShelfSvc'];
export default HomeController;
As you see, there is no difference in the way that we applied dependency injection — it is same as the way we do in ES5. We are exporting the HomeController class from this module. Check the code of AddBookController and ArchiveController. They follow a similar structure. The file bookShelf.controllers.js imports these controllers and registers them to a module. This is the code from this file:
import HomeController from './HomeController';
import AddBookController from './AddBookController';
import ArchiveController from './ArchiveController';

var moduleName='bookShelf.controllers';

angular.module(moduleName, [])
  .controller('bookShelf.homeController', HomeController)
  .controller('bookShelf.addBookController', AddBookController)
  .controller('bookShelf.archiveController', ArchiveController);

export default moduleName;
The bookShelf.controllers module exports the name of the AngularJS module it created, so that this can be imported into another module to create to create the main module.

Defining Services

“Service” is an overloaded term in general and in Angular as well! The three types of services used are: providers, services and factories. Out of these, providers and services are created as instances of types, so we can create classes for them. Factories are functions that return objects. I can think of two approaches for creating a factory:
  1. The same as in ES5, create a function which returns an object
  2. A class with a static method which returns an instance of the same class. This class would contain the fields that have to be exposed from the factory object
Let’s use the second approach to define a factory. This factory is responsible for interacting with the Express API and serving data to the controllers. The factory depends on Angular’s $http service to perform Ajax operations. As it has to be a private field in the class, we will define a WeakMap for it. The following snippet creates the factory class and registers the static method as a factory:
var moduleName='bookShelf.services';

const HTTP = new WeakMap();

class BookShelfService
{
  constructor($http)
  {
    HTTP.set(this, $http);
  }

  getActiveBooks(){
    return HTTP.get(this).get('/api/activeBooks').then(result => result.data );
  }

  getArchivedBooks(){
    return HTTP.get(this).get('/api/archivedBooks').then(result => result.data );
  }

  markBookRead(bookId, isBookRead){
    return HTTP.get(this).put(`/api/markRead/${bookId}`, {bookId: bookId, read: isBookRead});
  }

  addToArchive(bookId){
    return HTTP.get(this).put(`/api/addToArchive/${bookId}`,{});
  }

  checkIfBookExists(title){
    return HTTP.get(this).get(`/api/bookExists/${title}`).then(result =>  result.data );
  }

  addBook(book){
    return HTTP.get(this).post('/api/books', book);
  }

  static bookShelfFactory($http){
    return new BookShelfService($http);
  }
}

BookShelfService.bookShelfFactory.$inject = ['$http'];

angular.module(moduleName, [])
  .factory('bookShelfSvc', BookShelfService.bookShelfFactory);

export default moduleName;
This snippet uses the following additional features of ES6 (in addition to classes and arrow functions):
  1. A static member in the class
  2. String templates to concatenate the values of variables into strings

Defining Directives

Defining a directive is similar to defining a factory, with one exception — we have to make an instance of the directive available for later use inside the link function, because the link function is not called in the context of the directive object. This means that the this reference inside the link function is not the same as the directive object. We can make the object available through a static field. We will be creating an attribute directive that validates the title of the book entered in the text box. It has to call an API to check if the title exists already and invalidate the field if the title is found. For this task, it needs the service we created in the previous section and $q for promises. The following snippet creates a directive which it registers with a module.
var moduleName='bookShelf.directives';

const Q = new WeakMap();
const SERVICE = new WeakMap();

class UniqueBookTitle
{
  constructor($q, bookShelfSvc){
    this.require='ngModel';  //Properties of DDO have to be attached to the instance through this reference
    this.restrict='A';

    Q.set(this, $q);
    SERVICE.set(this, bookShelfSvc);
  }

  link(scope, elem, attrs, ngModelController){
    ngModelController.$asyncValidators.uniqueBookTitle = function(value){

      return Q.get(UniqueBookTitle.instance)((resolve, reject) => {
        SERVICE.get(UniqueBookTitle.instance).checkIfBookExists(value).then( result => {
          if(result){
            reject();
          }
          else{
            resolve();
          }
        });
      });
    };
  }

  static directiveFactory($q, bookShelfSvc){
    UniqueBookTitle.instance =new UniqueBookTitle($q, bookShelfSvc);
    return UniqueBookTitle.instance;
  }
}

UniqueBookTitle.directiveFactory.$inject = ['$q', 'bookShelfSvc'];

angular.module(moduleName, [])
  .directive('uniqueBookTitle', UniqueBookTitle.directiveFactory);

export default moduleName;
Here, we could have used ES6’s promise API, but that would involve calling $rootScope.$apply after the promise produces a result. The good thing is that promise API in AngularJS 1.3 supports a syntax similar to the ES6 promises.

Defining the Main Module and Config block

Now that we have modules containing the directives, controllers and services, let’s load them into one file and create the main module of the application. Let’s begin by importing the modules.
import { default as controllersModuleName } from './bookShelf.controllers';
import { default as servicesModuleName } from './bookShelf.services';
import { default as directivesModuleName } from './bookShelf.directives';
The config block defines routes for the application. This can be a simple function as it doesn’t have to return any value.
function config($routeProvider){
  $routeProvider
    .when('/',{
      templateUrl:'templates/home.html',
      controller:'bookShelf.homeController',
      controllerAs:'vm'
    })
    .when('/addBook',{
      templateUrl:'templates/addBook.html',
      controller:'bookShelf.addBookController',
      controllerAs:'vm'
    })
    .when('/archive', {
      templateUrl:'templates/archive.html',
      controller:'bookShelf.archiveController',
      controllerAs:'vm'
    })
    .otherwise({redirectTo:'/'});
}

config.$inject = ['$routeProvider'];
Finally, let’s define the main module and export its name. If you remember, this name is used in the bootstrap.js file for manual bootstrapping.
var moduleName = 'bookShelf';

var app = angular.module(moduleName, ['ngRoute','ngMessages', servicesModuleName, controllersModuleName, directivesModuleName])
  .config(config);

export default moduleName;

Conclusion

Hopefully this gives you an insight into using ES6 to write AngularJS apps. AngularJS 2.0 is being written completely using ES6 and as web developers we need to be aware of the way we have to write our code in the near future. ES6 solves many problems that have been bugging JavaScript programmers for years and using it with AngularJS is a lot of fun! And please remember, the sample code for this application can be found on our GitHub repository.

Frequently Asked Questions about Writing AngularJS Apps Using ES6

What are the benefits of using ES6 with AngularJS?

ES6, also known as ECMAScript 2015, introduces several new features and syntax improvements that can enhance your AngularJS applications. These include arrow functions, template literals, classes, modules, promises, and more. Using ES6 can make your code cleaner, more readable, and easier to maintain. It also allows you to take advantage of the latest JavaScript features and best practices, which can improve the performance and functionality of your AngularJS apps.

How do I set up my development environment to use ES6 with AngularJS?

To use ES6 with AngularJS, you’ll need to set up a development environment that supports ES6 syntax and features. This typically involves using a transpiler like Babel, which can convert ES6 code into ES5 code that can be run in current browsers. You’ll also need a module bundler like Webpack or Browserify to manage your JavaScript modules and dependencies.

Can I use ES6 classes with AngularJS?

Yes, you can use ES6 classes with AngularJS. ES6 classes provide a more concise and intuitive syntax for creating objects and dealing with inheritance. They can be used to define AngularJS components, services, and controllers, making your code more organized and easier to understand.

How do I use ES6 modules with AngularJS?

ES6 modules allow you to write modular code, which can be easier to manage and test. To use ES6 modules with AngularJS, you’ll need to export your AngularJS components, services, and controllers as ES6 modules, and then import them where they’re needed. This can be done using the export and import keywords in ES6.

What is the role of Babel in writing AngularJS apps using ES6?

Babel is a JavaScript transpiler that converts ES6 code into ES5 code, which is compatible with current browsers. This allows you to write your AngularJS apps using the latest ES6 features and syntax, while still ensuring that your apps can run in all browsers.

How can I use ES6 promises with AngularJS?

ES6 promises provide a more powerful and flexible way to handle asynchronous operations compared to traditional callback functions. You can use ES6 promises in your AngularJS apps by wrapping your asynchronous operations in a Promise object, and then using the then and catch methods to handle the results or errors.

Can I use ES6 arrow functions with AngularJS?

Yes, you can use ES6 arrow functions with AngularJS. Arrow functions provide a more concise syntax for writing function expressions, and they also have the benefit of not binding their own this value, which can be useful in certain situations.

What are the challenges of using ES6 with AngularJS?

While ES6 offers many benefits, there can be challenges in using it with AngularJS. These include setting up a development environment that supports ES6, dealing with browser compatibility issues, and learning the new ES6 syntax and features. However, these challenges can be overcome with the right tools and resources.

How can I use ES6 template literals with AngularJS?

ES6 template literals provide a more powerful and flexible way to create strings. You can use template literals in your AngularJS apps to create dynamic strings that include variables, expressions, and even multiline strings. This can be done using the backtick ( ) syntax in ES6.

Are there any resources to help me learn more about using ES6 with AngularJS?

Yes, there are many resources available to help you learn more about using ES6 with AngularJS. These include online tutorials, documentation, books, and courses. Some recommended resources include the ES6 section on the Mozilla Developer Network, the AngularJS documentation, and various online coding platforms.

Rabi Kiran (a.k.a. Ravi Kiran) is a developer working on Microsoft Technologies at Hyderabad. These days, he is spending his time on JavaScript frameworks like Angular JS, latest updates to JavaScript in ES6 and ES7, Web Components, Node.js and also on several Microsoft technologies including ASP.NET 5, SignalR and C#. He is an active blogger, an author at SitePoint and at DotNetCurry. He is rewarded with Microsoft MVP (ASP.NET/IIS) and DZone MVB awards for his contribution to the community.

angularAngular TutorialsColinIECMAScript6
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week