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 inbower.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
$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
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
$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 thelink
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.