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:
- Home page: Shows a list of active books. Books can be marked as read and moved to the archive from this page
- 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
- 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:
- Controllers using
$scope
- 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:
- Classes and WeakMaps, as already mentioned
- The arrow function syntax to register callbacks. The
this
reference inside the arrow functions is same as thethis
reference outside, which is the current instance of the class - 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:
- The same as in ES5, create a function which returns an object
- 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):
- A static member in the class
- 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.