Using ES6 in AngularJs 1.x with Browserify + Babel Build

In this article, we’ll look at how to write the component parts of AngularJs applications on ES6, then assemble using Browserify and Babel based on a small application that you can download from github and play around.

Writing Controller

A controller in AngularJs is a constructor function that can extend the scope being created either by injecting the $ scope parameter into the controller constructor, or by using the controller as approach. First, consider a more common approach through injecting $ scope using the registration controller as an example:
class SignupController {
    constructor($scope, $state, accountService) {
        this.init($scope, $state, accountService);
    }
    init($scope, $state, accountService) {
        $scope.signup = function () {
            accountService.signup().then(()=> {
                $state.go('main.list');
            });
        };
    }
}
SignupController.$inject = ['$scope', '$state', 'accountService'];
export {SignupController}

As you can see, the controller is represented by the ES6 class, which injects the dependencies of $ scope and two services into the constructor.

Here I want to immediately note that we have lost the ability to list dependencies using the inline array annotation, that is, like this:
someModule.controller('MyController', ['$scope', 'greeter', function($scope, greeter) {
  // ...
}]);
 

Thus, the ability to specify dependencies and their injection order remains only through the $ inject property, which is defined in the created SignupController class.

The second way to define a controller using the controller as approach looks more magical when combined with the ES6 class. And when writing a controller, I find it to be the most preferable.

var _state = new WeakMap();
var _accountService = new WeakMap();
class SigninController {
    constructor($state, accountService) {
        _state.set(this, $state);
        _accountService.set(this, accountService);
    }
    login() {
        _accountService.get(this).login().then(()=> {
            _state.get(this).go('main.list');
        });
    };
}
SigninController.$inject = ['$state', 'accountService'];
export {SigninController}

As you can see, the class has lost the explicit mention of scope, has become a bit more independent from AngularJs, and even dependencies are injected through the constructor. But now private variables appear in the class and with them the problem of their use within the class. Very readily written about this in the article " Implementing Private Fields Using WeakMap in JavaScript " and the best solution guaranteeing the release of resources and the belonging of variables only to this class would be to use WeakMap - of the minuses - we write a little more code, pluses - we sleep peacefully.

Now the last step is to declare Controller in the Angular module.

To do this, I created a separate module.js file, in which the ES6 modules are imported and registered in Angular modules.

import router from './router.js';
import {SigninController} from './controllers/signin/signin.controller.js';
angular.module('account').controller('SigninController', SigninController);

We write Provider, Factory, Service

The next step is to implement some class of business logic - in my case it will be the AccountService class.

It looks just as magical as the previous class - without any mention of AngularJs

import api from './accountApi.factory.js';
class AccountService {
    login(){
        return api.login();
    }
    signup(){
        return api.signup();
    }
}
export {AccountService}

Note that the AccountService class depends on the module declared in the accountApi.factory.js file, but the dependency is imported, not injected using the DI mechanism provided by AngularJs. In principle, the AccountService controller could be imported rather than injected into the controller described above. It all depends on how you want to build your application.

So the service class is described, now it remains to declare the service in the Angular module.

The Angular service is the easiest to declare. With Factory and Provider, things are a little more complicated.

We declare the service in our module.js file:

........
import {AccountService} from './services/accountService.factory.js';
.........
angular.module('account').service('accountService', AccountService);

Everything is simple here - an instance of the AccountService class will be created using the new operator, since the service method expects a constructor function.

What would the code look like if we needed to declare a provider:

angular.module('account').provider('accountService', providerBuilder(AccountService));
function providerBuilder(obj) {
    return function () {
         this.$get = [function () {
               return new obj();
               }];
             }
           }

And finally, if we needed a factory:

angular.module('account').factory('accountService', function(){return new AccountService()});

And it’s better to declare a static function in the AccountService class that will create an instance of the class and then the code will look like this:

angular.module('account').factory('accountService', AccountService.createInstance); 	

An example with similar behavior will be given below.

Writing a directive

directive will look like this:
var _accountService = new WeakMap();
class Copyright {
    constructor($templateCache, accountService) {
        _accountService.set(this, accountService);
        this.restrict = 'E';
        this.template = $templateCache.get('account/directives/copyright/copyright.directive.html');
        this.scope = {};
        this.controller = ['$scope', function ($scope) {
            $scope.copyright = function () {
                return 'Page  - 2015';
            };
        }];
    }
    link(scope) {
        scope.doSomething = function () {
            //какой-нибудь код
            var accountService= _accountService.get(Copyright.instance);
            //какой-нибудь код
        }
    }
    static createInstance($templateCache, accountService) {
        Copyright.instance = new Copyright($templateCache, accountService);
        return Copyright.instance;
    }
}
Copyright.createInstance.$inject = ['$templateCache', 'accountService'];
export {Copyright}

My directive does nothing, but has all the main parts.

In the class, I define all the standard directive fields that are needed and I want to focus your attention on how the directive is declared.
The directive is declared in the Angular module in much the same way as the factory, but there is one small difference: this in the constructor function will not be equal to this in the link function and therefore I save the link to this in the instance field of the class.

Similarly, you can declare filter, constant, and value.

Assembling the project

So, we wrote some code that is divided into ES6 modules and now we need to assemble it together. Browserify is responsible for finding module dependencies and assembling them into a single file. To do this, first of all, we will determine the entry point from which the assembly will begin.

I suggest defining 2 entry points — the entry point of the module — that is, it is a file that imports the modules / files of only its module and a common entry point that combines the entry points of the modules.

But I have only 1 module and therefore there will only be 2 such files:
  1. The module.js file located in the root of the account folder and having relative links to all used module files
  2. The app.js file located in the root of the application and having links to all the module.js files of the project

The second task is to convert code written on ES6 to ES5. This task will be performed by Babel, connected to Browserify as a plugin using the transform option.

You can find the code of the collector, as well as the code of the project in the github repository .

Literature:
  1. Guide to AngularJS Documentation
  2. Implementing private fields using WeakMap in JavaScript
  3. Fast browserify builds with watchify

Also popular now: