Angular 1 with ES6

27 September 2016

With the development of Angular 2 and it’s strong use of Typescript I thought I’d take some time to showcase some of the Javascript ES6 features and how they can be used right now with an (now old school) Angular 1 app.

Using transpilers such as Babel make it incredibly easy to add support for the ES2015 standard (i.e. ES6), and some experimental additions to the language that have yet to be finalised without the target browser necessarily supporting all of the new features yet.

In the provided project I make strong use of three big changes to the Javascript language in ES6: classes, imports, and arrow functions. The introduction of these three features not only make your Javascript code more readable (in my opinion), but also make it much easier to learn Javascript if you are coming from an object oriented background (Java, C#) and have had no prior Javascript experience.

Basic setup

For a basic ES6 Angular project I used the following:

  • Babel for compiling the ES6 Javascript code into ES5 browser readable Javascript.
  • Webpack for executing Babel on the ES6 code, creating sourcemaps, and providing an auto watch tool for compiling any changes made to the project (you can use Babel in the browser for real-time compiling but it is very slow so is not recommended).
  • NPM for dependencies.

My NPM package.json contains 3 Babel dependencies, 2 css loaders so that I can compile CSS files as well, and one dependency for Webpack:

{
  "name": "angular-es6",
  "version": "1.0.0",
  "dependencies": {
      ...
  },
  "devDependencies": {
    "babel-core": "^6.8.0",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "css-loader": "^0.23.1",
    "style-loader": "^0.13.1",
    "webpack": "^1.13.0"
  },
  "engines": {
    "node": ">=0.10.0"
  }
}

My webpack.config.js file contains an app entry point (an angular module file), and any loaders I wish to use.

module.exports = {
    entry: "./app/module.js",
    devtool: 'source-map',
    output: {
            path: __dirname,
            filename: "./dist/bundle.js"
    },
    module: {
        loaders: [
            {
                test: /\.css$/,
                loader: "style!css"
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: "babel-loader",
                query: {
                        presets: ['es2015']
                }
            }
        ]
    }
};

A Webpack loader will be used to compile a file contained in a require or import statement in the Javascript code if it matches the ‘test’ regular expression. So all CSS files will use the styles loader, while Javascript files will use the babel-loader.

Simply running webpack --watch I can tell webpack to watch the ./app/module.js file for changes, along with any files it imports, and compile it into a ./dist/bundle.js file.

Basic Angular setup

Now that our simple build system has been setup we can start writing some Angular code in ES6. I have created a project that will compile and execute ES6 code on the fly within the web browser which also includes some interesting new ES6 features that can be played about with.

To create our Angular app we must first download our Angular dependencies from NPM or bower (I used NPM in this case).

npm install angular angular-route bootstrap --save

Which will automatically add these dependencies to our package.json file as web dependencies. Note if you are using source control, do not commit your node_modules or bower directories as they can become very large and are not needed in source control.

...
"dependencies": {
    "angular": "^1.5.5",
    "angular-route": "^1.5.5",
    "bootstrap": "^3.3.6"
  },
...

Then we can create our Angular module. This is where we import all of our controller, service, configuration, and directive files and register them with the angular module.

import angular from 'angular';
import angularRoute from 'angular-route';

import MainStyle from './main.css';

import Router from './router';

import AllowTabDirective from './directives/allow-tab/allow-tab.directive';

import CompilerService from './services/compiler/compiler.service';
import DebounceService from './services/debounce/debounce.service';
import ExamplesService from './services/examples/examples.service';

import CompilerController from './controllers/compiler/compiler.controller';

angular.module('angular-es6', ['ngRoute'])
    .config(Router)

    .directive('allowTab', () => new AllowTabDirective())

    .service('CompilerService', CompilerService)
    .service('DebounceService', DebounceService)
    .service('ExamplesService', ExamplesService)

    .controller('CompilerController', CompilerController);

If you’ll notice the class can be passed directly into the angular module in most cases, except in the case of the directive where we want to create a new directive each time it is called, so we wrap it in an arrow function. This is so that everytime angular sees a directive declaration it creates a new instance of the directive class.

() => new AllowTabDirective()

which is the equivalent in ES5 to:

function() {
    return new AllowTabDirective();
}

Config

For the configuration I added a basic router.

import CompilerController from './controllers/compiler/compiler.controller';

export default function routerConfig ($provide, $routeProvider) {
  $provide.factory('$routeProvider', function () {
      return $routeProvider;
  });

  $routeProvider
    .when('/', {
      name: 'compiler',
      templateUrl: CompilerController.getTemplateUrl(),
      controllerAs: CompilerController.getControllerTemplateName(),
      controller: CompilerController,
    })
    .otherwise({
      redirectTo: '/'
    });
}

There are a couple things of interest here. Using the ES6 module exporter I can declare a default value to export from each file I create. In this case I am importing the class CompilerController, and exporting a routerConfig function. You can export any number of variables, const, functions, or classes within a file, but if they are not identified as default then they will need to be named explicitly when importing the file.

Also do you notice the static function calls that I make on the CompilerController? These are using the inheritance system in ES6 to add some runtime checks to ensure a template url is provided for each controller.

Classes

Classes are defined as normal classes (they share a similar syntax to services), with Angular injections happening in the constructor rather than as function params.

export default class BaseController {
    constructor() {

    }

    static getTemplateUrl() {
        throw `No template url set for class ${this.name}, please add a getTemplateUrl() function in the ${this.name} class to return a valid template url.`;
    }

    static getControllerTemplateName() {
        return 'controller';
    }
}
import BaseController from '../base.controller';

export default class CompilerController extends BaseController {
  constructor (InjectedService) {
    super();
    this.InjectedService = InjectedService;
  }

  ...

  static getTemplateUrl() {
    return 'app/controllers/compiler/compiler.html';
  }
}

If you have used any object oriented languages before this syntax will be familiar to you. I created a base class with some static helper methods, and then extended this class in the CompilerController. I can then override the base classes functions to change the functionality.

The $scope service can also be replaced by a reference to this in the class, though you can still access the $scope service by injecting it in your controller's constructor. This is up to personal perfence, some people prefer using $scope while some prefer using this (though $scope must still be used when using event listeners).

export default class MyController {
    constructor($scope) {
        $scope.myModelValue = ‘Hi’;

        this.myModelValue === ‘Hi’; // true
    }
}

Services

Services are defined just as normal classes and registered on the Angular module at startup as shown above.

export default class MyService {
    constructor($location) {
        this.$location = $location;
    }

    getPath() {
        return this.$location.path();
    }
}

Directives

Directives have a slightly different setup to Controllers and Services.

export default class MyDirective {
  constructor () {
    return {
      restrict: 'E',
      templateUrl: 'app/directives/my-directive/my-directive.html',
      scope: {
              myBoundValue: '='
      },
      controller: MyDirectiveController,
      controllerAs: 'controller',
      bindToController: true
    };
  }
}

class MyDirectiveController {
  constructor (MyService) {
    this.myBoundValue !== undefined; // true
  }

  myFunction() {
    ...
  }
}

I defined the directive configuration in the default exported class MyDirective, which contains a reference to a controller MyDirectiveController. The controller will have the scope automatically injected into it as part of the directive configuration bindToController: true.

If you require a compile function you must still use a directive function rather than class syntax as the angular injector doesn't recognise the link function (see 'Creating a Directive that Manipulates the DOM' for more info). Fortunately you can still use the ES6 string templating to make your compile functions a bit more readable.

ES6

export default function (UserResource) {
  return {
    restrict: 'E',
    compile: (element, attrs) => {
      let user = UserResource.getUserDetails();
      element.replaceWith(`<div>${user} <img src="${user.image}"></img></div>`);
    }
  }
}

ES5

app.directive('itemControls', function(UserResource) {
  return {
    restrict: 'E',
    compile: function (element, attrs) {
      var user = UserResource.getUserDetails();
      element.replaceWith('<div>' + user + '<img src="' + user.image + '"></img></div>');
    }
  };
})

Putting it all together

Using these tools I created an Angular project that compiles ES6 code in real time in the browser, with some interesting examples showcasing some new features that will be coming to browsers in the future. The code can be found here, and the live project page here. Feel free to pull down the example code and use it as a basis for your own ES6 angular apps that you can build right now.

Screenshot of example app

Screenshot
Article By
blog author

Neil Armstrong

Senior Engineer