Writing AngularJS Apps Using ES6

Originally published at: http://www.sitepoint.com/writing-angularjs-apps-using-es6/

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 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 that convert ES6 code to ES5 compatible code. Community members have started playing around ES6 and many of them are blogging what they learn. SitePointā€™s JavaScript channel also has a good number of articles describing different features of ES6.

It is possible to write any piece of JavaScript that we are writing everyday 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 can we use features of ES6 to build different pieces of an AngularJS application and load them using ES6 modules to make them work on a page. The sample code contains a simple online book shelf application. We will see how it is structured and written.

A Note on the BookShelf application

The sample BookShelf application contains following views:

  1. Home page: Shows a list of active books and books can be marked as read and moved to archive from this page
  2. Add book page: Adds a new book to the shelf by accepting title of the book and name of author. It doesnā€™t allow a duplicate title
  3. Archive page: Lists all archived books

Setting up 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 traceur client side library to compile ES6 script on the fly and run it on the browser. This library is available on bower. Sample code has an entry for this library in bower.json.

In the home page of the application, we need to add a reference to this library and add the following script to it:

traceur.options.experimental = true;
new traceur.WebPageTranscoder(document.location.href).run();

JavaScript code of the application is divided into multiple files. These files are loaded into the main file using 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 bootstrap the Angular app. We cannot use ng-app to bootstrap the application as modules are loaded asynchronously. Following is the script in this 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 content of the bookShelf.main.js file later. The bootstrap.js file is loaded in 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 ā€œcontroller asā€ syntax

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 would be visible through alias of the controller. In addition, the ā€œcontroller asā€ syntax is comparatively less coupled with $scope. If you are not aware, $scope would 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 for 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 the module level and not including them in export object.

Continue reading this article on SitePoint
1 Like

Sample code of this article can be found on this GitHub repository: https://github.com/sravikiran/Angular-ES6-BookShelf

Hi, when trying to run the demo app I get this error in the JS console and none of the app works:

Uncaught TypeError: Cannot set property 'experimental' of undefined (index):61 (anonymous function)

The line in the code with the error looks like this:

traceur.options.experimental = true;

traceur is being loaded and the HTTP request is a 200. When I open the console and look to see what traceur.options is, itā€™s undefined.

Thatā€™s weird, I never saw that error.

It looks like an issue with the traceur. Can you try to manually delete the traceur folder from bower_components and install the package again and try running the sample?

S-

Going to be starting a fresh new project next week in angular 1.3. Do you think i should use the es6 approachā€¦ or at this point am i asking for major issues???

i want to move to angular 2 when released so i am very tempted to follow the standardā€¦ is it safe to do soā€¦

Thatā€™s a good question.

Yes, using ES6 today with Angular 1.3 will make the process of migrating to Angular 2 easier. At the same time, as the specification is yet to be finalized and to be implemented by browsers, you need to use a transpiler like Traceur or Babel to make the browsers understand your code.

Also, start using the features of Angular like ā€œcontroller asā€ syntax and also keep updating to the new releases 1.x of as the team promises that updating will make it easier to move to Angular 2.

Good luck!

Question: for the directive, why store the new instance on the class instead of just returning the new instance? For the service you just return the new instance. Is there a reason to store it for the directive?

UniqueBookTitle.instance =new UniqueBookTitle($q, bookShelfSvc);
return UniqueBookTitle.instance;

instead of

return new UniqueBookTitle($q, bookShelfSvc);

Hi rhodesjason, I am storing the instance to make it available to the link function. Link is not called with the context of the directive object. refer to the first paragraph under Defining Directives section, it answers your question.

But, if Iā€™m reading this right, you will only have access to the most recently created instance. So if you have more than one instance of the directive, wonā€™t your link function almost always be tied to the wrong instance?

Also, if I remember right, anything that is injectable is a singleton, so all class instances will be referring to the the same instances of the dependencies. You could just keep track of the dependencies at the class level, not the instance level.

Directives are singletons as well, so I donā€™t think there is a possibility of one more instance of the directive being created unless you register another directive using the same class. I see a scope for improvement to the code though. I should have checked if the static property instance is already assigned with an object before assigning an instance.

Yes, you can set constructor as the target for DI. For that, you will have to create the object using $injector.instantiate. Otherwise, get references of the dependencies using $injector and then pass them into the constructor.

Great article, really help me to start new Angular project using ES6 code style.
How do you define more the 1 service?

You can use the same approach in which I wrote the service to write another service. If you want to keep them together, add a folder for services and add all services of your application in that folder, It is the same way that I used for controllers. If size of the application grows, it is better to create folders based on domain and add your receipes in the domain specific folders.

1 Like

Can you give an example, I tried to write somthing like this:

import DataService from './services/data.srv';

var moduleName = 'nodeUi.services';

angular.module(moduleName,[])
  .services('nodeUi.dataService',DataService);

export  default moduleName;

without any luck. Can you let me know what is wrong?

This looks good. Did you pass the module exported from this file as dependency into the main module?

yes, this is my index.js file:

'use strict';

import {default as controllersModule} from './nodeUi.controllers';
import {default as servicesModule} from './nodeui.services';

var moduleName = 'nodeUi';

function config(RestangularProvider,$urlRouterProvider,$stateProvider) {
  RestangularProvider.setBaseUrl('http://127.0.0.1:1337');
  $stateProvider
    .state('login', {
      url: '/',
      templateUrl: 'app/login/login.html',
      controller: 'nodeUi.loginCtrl',
      controllerAs: 'loginCtrl'
    });

  $urlRouterProvider.otherwise('/');
}
angular.module(moduleName, ['ngAnimate', 'ngCookies', 'ngSanitize', 'ui.router', 'ngMaterial','restangular',servicesModule,controllersModule])
  .config(config);

config.$inject = ['RestangularProvider','$urlRouterProvider','$stateProvider'];

export default moduleName;

but now, when I tried to use the service (ā€˜dataServiceā€™) in my controller:

'use strict';
const SERVICE = new WeakMap();
class LoginCtrl {
  constructor (dataService) {
    SERVICE.set(this, dataService);
    this.userLoggedin = false;
  }
    submitLogin()
    {
      SERVICE.get(this).getClusters().then(clusters => {
        this.clusters = clusters;
        this.userLoggedin = true;
      });
    }
}
LoginCtrl.$inject = ['dataService'];

export default LoginCtrl;

I get the exception :

 Unknown provider: dataServiceProvider <- dataService <- nodeUi.loginCtrl

Name of your service is nodeUi.dataService, and you mentioned it as dataService in the $inject statement. Correct it, it should work.

I changed it to:

LoginCtrl.$inject = ['nodeUi.dataService'];

now I get:

[ng:areq] Argument 'fn' is not a function, got string

I canā€™t help beyond this on a chat like this, I need to run the code. I would encourage you to debug and fix the issue though!

OK, fixed
Can see the code in https://github.com/omer72/Angular-ES6-BookShelf

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.