ES6: Jump in, the water is warm!

by
R. Mark Volkmann, Partner
Object Computing, Inc. (OCI)

All example code and configuration files that appear in this article are in GitHub at https://github.com/mvolkmann/todo-es6.

ECMAScript 6

The JavaScript language is defined by the ECMAScript specification, also known as ECMA-262. ECMAScript 6 (ES6) defines the next version of JavaScript. ECMA Technical Committee 39 (TC39) has a goal to complete the ES6 specification by the end of 2014. The current draft state of the specification can be viewed at http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts.

The number of new features being added in ES6 is much greater than the number added in ES5. Unlike ES5, some of the ES6 features involve new language syntax. ES6 will be backward compatible with ES5 which is backward compatible with ES3. Wikipedia provides a description of what happened to ES4 under "ECMAScript".

There are many articles and videos on the web that describe the ES6 features. A good place to start is Luke Hoban's summary at https://github.com/lukehoban/es6features.

It may take years for all the features in ES6 to be supported in all the major browsers. That's too long to wait and you don't have to wait. You can use a transpiler (described below) today. This will enable getting comfortable with the new features sooner and will allow writing more compact, more expressive code now.

For a summary of ES6 feature support in browsers (and in the Traceur tool discussed next), see the ES6 compatibility table from Juriy Zaytsev (a.k.a. kangax) at http://kangax.github.io/es5-compat-table/es6/. Try selecting the "Sort by number of features?" checkbox. Currently Firefox has a clear lead in ES6 support.

Transpilers

Compilers translate code in some programming language to another form with a lower-level of abstraction. For example, Java code is compiled to bytecode. Transpilers translate code in some programming language to another form with basically the same level of abstraction. For example, CoffeeScript code is transpiled to JavaScript.

There are several available transpilers that translate ES6 code to ES5.

Google Traceur

Probably the most capable transpiler for ES6 to ES5 translation in terms of feature support is Google Traceur. Traceur is implemented in ES6 and uses itself to transpile its own ES6 code to ES5 code that runs on Node.js. For more detail, see https://github.com/google/traceur-compiler.

There is an online tool at http://google.github.io/traceur-compiler/demo/repl.html that allows entering ES6 on the left side and shows the resulting ES5 code on the right. This is useful for testing support for specific ES6 features and gaining an understanding of what Traceur generates. It does not execute the code. The “Options” menu includes the ability to enable experimental features.

The easiest way to install Traceur is to install Node.js and then run this command:

npm install -g traceur

To get help on options:

traceur --help
traceur --longhelp

To run code in an ES6 file:

traceur {es6-file-path}

This command requires the file extension to be .js, but it can be omitted in the command.

To compile an ES6 file to an ES5 file:

traceur --script {es6-file-path} --out {es5-file-path}

The generated code depends on the provided file traceur-runtime.js. This file can be copied from the directory where Traceur is installed. Assuming it was installed globally, determine the location by running npm root -g. The file will be below that directory in traceur/bin. To use the generated code in a browser, include a script tag for traceur-runtime.js.

To run with experimental features, add the --experimental option. One example of a feature that is currently considered experimental is the let keyword.

Sourcemaps

Sourcemaps allow browser debuggers to step through code that was transpiled from another language into the actual JavaScript code that runs in the browser. For example, they can be used to debug CoffeeScript code. They can also be used to debug ES6 code that was transpiled to ES5.

The traceur option --sourcemap causes it to generate a sourcemap. It places them in the same directory as generated ES5 files. The browser will look for them there.

To use sourcemaps in Chrome,

  1. open Developer Tools
  2. click the gear icon in the upper-right
  3. check "Enable JS source maps"
  4. check "Search in content scripts"
  5. select ES6 .js files from the “Sources” tab
  6. set breakpoints
  7. refresh the page

In Firefox, sourcemaps are enabled by default. To open the Firefox debugger, select Tools...Web Developer...Debugger.

Linting

Many JavaScript developers, including me, feel that it is important to use some linting tool when writing JavaScript code. Linting tools save time and reduce errors by catching coding issues before the code is run. They can be run from a command-line, integrated into editors/IDEs, and run automatically when files are saved from any editor using tools like Grunt.

The three most popular JavaScript linting tools seem to be JSLint (http://jslint.org), JSHint (http://jshint.org), and ESLint (http://eslint.org). It is unclear if or when JSLint will support ES6. JSHint has good support now using the "esnext" option. ESLint plans to support ES6, but doesn't yet.

I highly recommend using JSHint to check ES6 code!

Here is the JSHint configuration file I use:

.jshintrc

{
  "browser": true,
  "devel": true,
  "esnext": true,
  "indent": 2,
  "jquery": false,
  "maxlen": 80,
  "node": true
}

Grunt

Grunt is a great tool for automating web development tasks. There are over 2,500 plugins available. Several are related to Traceur. These include "traceur", "traceur-latest", "traceur-build", "traceur-simple", and "node-traceur".

Below is an example Gruntfile.js that uses "traceur-simple" to automate transpiling ES6 files into ES5 when they are modified. The lines that are specific to Traceur are highlighted in the online version of this article at http://sett.ociweb.com/settApr2014.html.

This example also uses the "watch" plugin to watch for changes to HTML, CSS and JavaScript files. When watch detects these, it automatically runs specified tasks including linting CSS and JavaScript, running Traceur to generate ES5 code, and refreshing the browser to immediately show the results of the changes. This last part is enabled by the "livereload" option and including a special script tag in the main HTML file.

Gulp, at http://gulpjs.com is a similar tool. I used Grunt in the example presented here, but I welcome a pull request to the GitHub repo with an equivalent gulpfile.js file.

module.exports = function (grunt) {
  grunt.initConfig({
    clean: ['build'],
    connect: { // serves static files
      server: {
        options: {
          port: 3000,
          base: '.'
        }
      }
    },
    csslint: {
      strict: {
        options: {
          ids: false // allows ids to be used in CSS selectors
        },
        src: ['styles/*.css']
      }
    },
    jshint: {
      options: {
        jshintrc: '.jshintrc'
      },
      all: ['Gruntfile.js', 'scripts/**/*.js']
    },
    traceur: {
      options: {
        // This option includes runtime code in the generated file.
        //includeRuntime: true,
        traceurOptions: '--experimental --sourcemap'
      },
      all: {
        files: {
          // Just need to transpile main file which imports others.
          'build/app.js': ['scripts/app.js']
        }
      }
    },
    watch: {
      options: { livereload: true },
      css: {
        files: ['styles/*.css'],
        tasks: ['csslint']
      },
      html: {
        files: ['*.html'],
        tasks: [] // just watching for changes
      },
      js: {
        files: ['Gruntfile.js', 'scripts/**/*.js'],
        tasks: ['jshint', 'traceur']
      }
    }
  });

  require('matchdep').filterDev('grunt-*').
    forEach(grunt.loadNpmTasks);

  grunt.registerTask('default',
    ['csslint', 'jshint', 'traceur', 'connect', 'watch']);
};

Example

Here is a basic todo web application written using AngularJS and ES6. Several features of ES6 are utilized including arrow functions, default parameters, for-of loops, let, classes, modules, and generators. For a small application like this, I likely would not split the code into this many source files and use as many classes. This is done to more clearly demonstrate the use of modules.

A major goal of this example is to demonstrate that ES6 can be used in conjuction with existing JavaScript libraries like AngularJS today.

To run this app:

  1. install git if not already installed
  2. install Node.js if not already installed
  3. git clone https://github.com/mvolkmann/todo-es6
  4. cd to the repo directory
  5. npm install to install Grunt plugins (includes grunt-traceur-simple and Traceur itself)
  6. npm install -g grunt-cli
  7. grunt to start server
  8. browse localhost:3000

Screenshot

Todo app screenshot

While AngularJS is not the focus of this article, here is a brief explanation of the AngularJS concepts that are used in index.html below. The ng-app attribute on line 2 identifies the main AngularJS module. That is defined on line 8 in scripts/app.js. The ng-controller attribute on line 12 in index.html identifies the AngularJS controller that is responsible for the "scope" of the entire document. That is defined on lines 13-20 in scripts/app.js. The main job of an AngularJS controller is to add data and functions to the scope. The expressions in double curly braces refer to scope properties. When the value of one of these expressions changes, that part of the DOM is automatically updated. The ng-click attributes specify scope expressions to be evaluated when the associated element is clicked. The ng-model attributes specify two-way data bindings. When the specified scope property value changes, the value of the corresponding input is updated. When the user enters a new value in the input, the corresponding scope property is updated. The CSS class on the span that displays the text of a todo item is changed when the "done" state of the todo item is changed.

For more details on AngularJS, see http://angularjs.org.

index.html

<!DOCTYPE html>
<html ng-app="Todo">
  <head>
    <link rel="stylesheet" href="styles/todo.css">
    <script
      src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js">
    </script>
    <script src="lib/traceur-runtime.js"></script>
    <script src="build/app.js"></script>
    <script src="http://localhost:35729/livereload.js"></script>
  </head>
  <body ng-controller="TodoCtrl">
    <h2>To Do List</h2>
    <div>
      {{todoList.getUncompletedCount()}} of {{todoList.length}} remaining
      <button ng-click="todoList.archiveCompleted()">
        Archive Completed
      </button>
    </div>
    <br>

    <!-- Wrapping this in a form causes the button to be activated
         when the input has focus and the return key is pressed. -->
    <form>
      <input type="text" ng-model="todoText" size="30"
        placeholder="enter new todo here" autofocus>
      <button ng-click="addTodo()" ng-disabled="!todoText">
        Add
      </button>
    </form>

    <ul class="unstyled">
      <li ng-repeat="(timestamp, todo) in todoList.todos">
        <input type="checkbox" ng-model="todo.done">
        <span class="done-{{todo.done}}">{{todo.text}}</span>
        <button ng-click="todoList.delete(todo)">Delete</button>
      </li>
    </ul>
  </body>
</html>

styles/todo.css

body {
  padding-left: 10px;
}

h2 {
  color: blue;
}

.unstyled {
  list-style: none;
  padding-left: 0;
}

.done-true {
  color: gray;
  text-decoration: line-through;
}

scripts/app.js

'use strict';
/*global angular: false */

// Without the ./ in in the next line, Traceur looks for todolist.js
// in the build directory instead of the scripts directory.
import TodoList from './todolist';

var app = angular.module('Todo', []);
var todoList = new TodoList();
todoList.add('learn AngularJS', true);
todoList.add('build an AngularJS app');

app.controller('TodoCtrl', $scope => {
  $scope.todoList = todoList;

  $scope.addTodo = () => {
    todoList.add($scope.todoText);
    $scope.todoText = ''; // clears input
  };
});

scripts/todolist.js

import Todo from './todo';
import {values} from './generators';

class TodoList {
  constructor() {
    // If Traceur supported Maps, this could be a Map with integer keys.
    this.todos = {}; // map of Todo objects by timestamp
    this.length = 0;
  }

  add(text, done = false) {
    let todo = new Todo(text, done);
    this.todos[String(todo.timestamp)] = todo;
    this.length++;
  }

  archiveCompleted() {
    // Not saving completed todos in this version.
    for (let todo of values(this.todos)) {
      if (todo.done) this.delete(todo);
    }
  }

  delete(todo) {
    delete this.todos[String(todo.timestamp)];
    this.length--;
  }

  getUncompletedCount() {
    // Unlike this.length, this must be recalculated because
    // AngularJS changes the done property in the Todo objects
    // when checkboxes are checked.
    // If Traceur supported proxies, we could track
    // changes to the done properties.
    let count = 0;
    for (let todo of values(this.todos)) {
      if (!todo.done) count++;
    }
    return count;
  }
}

// Having a "default" export is useful when
// there is one main thing a module exports.
export default TodoList;

scripts/todo.js

class Todo {
  constructor(text, done = false) {
    this.text = text;
    this.done = done;

    var ts = Date.now(); // used as unique identifier
    // Adjust if this Todo was created in the
    // same millisecond as the previous one.
    this.timestamp = ts === Todo.lastTs ? ts + 1 : ts;
    Todo.lastTs = this.timestamp;
  }
}

export default Todo;

scripts/generators.js

This module exports several functions and does not define a "default" export.

// This module defines several useful generators,
// not all of which are used in the Todo app.

// A generator for iterating over the key/value pairs in an object.
export function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}

// A generator for iterating over the keys in an object.
export function* keys(obj) {
  for (let key of Object.keys(obj)) {
    yield key;
  }
}

// A generator that yields the first n values of an iterator.
function* take(iterator, n) {
  while (n > 0) {
    yield iterator.next();
    n--;
  }
}

// A generator for iterating over the values in an object.
export function* values(obj) {
  for (let key of Object.keys(obj)) {
    yield obj[key];
  }
}

Summary

So which features of ES6 should you start using today? I recommend choosing those in the intersection of the set of features supported by Traceur and the set of features supported by JSHint. The intersection includes at least these:

References


Valid XHTML 1.0 Strict [Valid RSS]
RSS
Top