In this codelab, you'll learn how to write a JavaScript application using the ES2015 syntax - formally known as ES6 - that runs natively in Chrome. We'll convert an app's ES5 code to ES2015 and discuss the differences and advantages of the new syntax.

What you'll learn

What you'll need

How will you use this tutorial?

Read it through only Read it and complete the exercises

What is your current level of experience with JavaScript?

Novice Intermediate Proficient

What is your current level of experience with ES2015?

Novice Intermediate Proficient

You can do this codelab using any text editor you like. If you don't have another editor handy, you can download the Chrome Dev Editor.

Get the initial source code

In this codelab, we'll be converting an existing app's source code from ES5 to ES2015. Get the app's code by running:

git clone https://github.com/googlecodelabs/chrome-es2015

Alternatively you can also download it as a Zip file using the button below and unzip it:

Download the ES2015 Codelab's code


From the chrome-es2015 directory run npm install from the command line (you might have to sudo). This installs a lightweight web server called serve into the node_modules/ folder.

Your folder structure should now look like this:

chrome-es2015/
  node_modules/  <!-- Where npm saves the dependencies/devtools -->
  scripts/  <!-- The app's JS source files -->
    main.js <!-- The app's JS code -->
    main.es6_FINAL.js  <!-- The app's JS code converted to ES2015 -->
  index.html  <!-- The app's main UI -->
  main.css <!-- The app's stylesheet -->
  package.json  <!-- Npm metadata file used for managing deps -->
  ...


The app's JavaScript source files are located under script/. Most of the codelab focuses on converting the existing ES5 code in scripts/main.js to ES2015 syntax.

Preview the app

Running npm install earlier should have installed serve, a lightweight web server. Now run npm start and the app's code will be served from http://localhost:52587. Open that URL in Chrome; this is what you should see:

The app we'll be converting for this codelab is called Simple Sticky Note. It lets you add notes to the page and saves all of these to local storage. Now try to add a Note:

Clicking the ADD button will add the note as a card below:

Now let's have a look at the scripts/main.js file. This is where the JavaScript code of the app is located. In the next steps you'll port this code to the new ES2015 syntax. This syntax is well supported by modern browsers such as Chrome. .

Copy the scripts/main.js file to scripts/main.es6.js. Don't forget to change the script import statement in your index.html:

index.html

...

<script src="scripts/main.es6.js"></script>
</body>
</html>

After each of the following steps just reload the app on Chrome and make sure the app still works as expected.

One of the main addition in ES2015 is the new class syntax. The new class syntax doesn't actually change any of the existing behavior you could achieve by using prototype, but it brings a much clearer and simpler way of defining classes.

Our Sticky Note app is already built in an object-oriented way (using prototype) by defining a StickyNotesApp class that puts the app together and a StickyNote class that defines the custom element for sticky notes.

Let's make the StickyNotesApp class declaration use the new class syntax in scripts/main.js. This is how the class is currently declared in ES5:

main.js

// Initializes the Sticky Notes app.
function StickyNotesApp() {
  ...
}

// Saves a new sticky note on localStorage.
StickyNotesApp.prototype.saveNote = function() {
  ...
};

// Resets the given MaterialTextField.
StickyNotesApp.resetMaterialTextfield = function(element) {
  ...
};

// Creates/updates/deletes a note in the UI.
StickyNotesApp.prototype.displayNote = function(key, message) {
  ...
};

// Enables or disables the submit button depending on the values of the input field.
StickyNotesApp.prototype.toggleButton = function() {
  ...
};

You'll notice that resetMaterialTextfield is static. We'll now define the same class but using the new class syntax — and get rid of all those prototypes. You can do this in three steps:

main.es6.js

// A Sticky Notes app.
class StickyNotesApp {

  // Initializes the Sticky Notes app.
  constructor() {
    ...
  }

  // Saves a new sticky note on localStorage.
  saveNote() {
    ...
  }

  // Resets the given MaterialTextField.
  static resetMaterialTextfield(element) {
    ...
  }

  // Creates/updates/deletes a note in the UI.
  displayNote(key, message) {
    ...
  };

  // Enables or disables the submit button depending on the values of the input field.
  toggleButton() {
    ...
  }
}

Make sure the app still works by reloading it on Chrome and keep on doing this after each steps.

Now let's make the StickyNotes custom element class declaration use the new class syntax as well. Note a few things:

This is how the class is currently declared in ES5:

main.js

// A Sticky Note custom element based on HTMLElement.
var StickyNote = Object.create(HTMLElement.prototype);

// Initial content of the element.
StickyNote.TEMPLATE = ...

// StickyNote elements top level style classes.
StickyNote.CLASSES = ...

// List of shortened month names.
StickyNote.MONTHS = ...

// Fires when an instance of the element is created.
StickyNote.createdCallback = function() {
  ...
};

// Fires when an attribute of the element is added/deleted/modified.
StickyNote.attributeChangedCallback = function(attributeName) {
  ...
};

// Sets the message of the note.
StickyNote.setMessage = function(message) {
  ...
};

// Deletes the note by removing the element from the DOM and the data from localStorage.
StickyNote.deleteNote = function() {
  ...
};

In ES2015 using the new class syntax you can simply extend another class using the extends notation. You can do this in three steps:

ES2015 did not define any new way to declare static or class fields. You will have to stick to the former syntax (e.g. StickyNote.TEMPLATE = ...). But maybe we'll get this in ES2016/ES7.

main.es6.js

// This is a Sticky Note custom element.
class StickyNote extends HTMLElement {

  // Fires when an instance of the element is created.
  createdCallback() {
    ...
  }

  // Fires when an attribute of the element is added/deleted/modified.
  attributeChangedCallback(attributeName) {
    ...
  }

  // Sets the message of the note.
  setMessage(message) {
    ...
  }

  // Deletes the note by removing the element from the DOM and the data from localStorage.
  deleteNote() {
    ...
  }
}

// Initial content of the element.
StickyNote.TEMPLATE = ...

// StickyNote elements top level style classes.
StickyNote.CLASSES = ...

// List of shortened month names.
StickyNote.MONTHS = ...

Also make sure you change the custom element's declaration as the syntax is different with classes. Replace this:

main.js

document.registerElement('sticky-note', {
  prototype: StickyNote
});

With this:

main.es6.js

document.registerElement('sticky-note', StickyNote);

Another convenient feature of ES2015 is arrow functions. Arrow functions can be used in place of anonymous functions. The syntax is a lot shorter and less cumbersome. Also arrow functions are automatically bound to this (no more .bind(this) on your functions).

We use anonymous functions in a couple places in the Sticky Notes app. Let's replace these with arrow functions:

main.es6.js

constructor() {

  ...

  // Listen for updates to notes from other windows.
  window.addEventListener('storage', e => this.displayNote(e.key, e.newValue));
}

...

// On load start the app.
window.addEventListener('load', () => new StickyNotesApp());

You can also use arrow function autobinding instead of named function binding.

Replace this:

main.js

function StickyNotesApp() {

  ...

  // Saves notes on button click.
  this.addNoteButton.addEventListener('click', this.saveNote.bind(this));

  // Toggle for the button.
  this.noteMessageInput.addEventListener('keyup', this.toggleButton.bind(this));

  ...

}

...

createdCallback() {

  ...

  this.deleteButton.addEventListener('click', this.deleteNote.bind(this));
}

With this:

main.es6.js

constructor() {

  ...

  // Saves notes on button click.
  this.addNoteButton.addEventListener('click', () => this.saveNote());

  // Toggle for the button.
  this.noteMessageInput.addEventListener('keyup', () => this.toggleButton());

  ...

}

...

createdCallback() {

  ...

  this.deleteButton.addEventListener('click', () => this.deleteNote());
}

When you declare a variable with var in JavaScript it has function scope — but most of the time you actually want block scope. The let keyword allows you to declare a block scope variable. Block scoping is how variables work in many other programming languages such as Java or C++.

In general it's good practice to always use let instead of var since it usually makes for clearer, more robust code and avoids some potential issues with variable name collision.

So let's change all the var to let:

main.es6.js

...

  for (let key in localStorage) {

...

  let key = Date.now().toString();

...

  let note = document.getElementById(key);

...

attributeChangedCallback(attributeName) {
  if (attributeName == 'id') {
    let date;
    if (this.id) {
      date = new Date(parseInt(this.id));
    } else {
      date = new Date();
    }
    let month = StickyNote.MONTHS[date.getMonth()];

...

You are now only using block scope variables. In most cases this doesn't really change the behavior of your function as you may have only been using the variable inside its own block anyway, but in some cases this behavior changes. For instance, we've purposely used non-block-scoped var in the attributeChangedCallback function. As you can see above, you have to move the date variable declaration outside of the inner blocks since the variable is used outside this block.

ES2015 comes with yet another awesome feature called template strings which allow you to do string interpolation and multi-lines strings. First let's change the StickyNote.TEMPLATE — which is a multi-line string — to use the more convenient template strings:

main.es6.js

// Initial content of the element.
StickyNote.TEMPLATE = `
   <div class="message"></div>
   <div class="date"></div>
   <button class="delete mdl-button mdl-js-button mdl-js-ripple-effect">
     Delete
   </button>`;

Second, in the attributeChangedCallback function, string concatenation can be replaced with a template string using interpolation:

main.es6.js

// Fires when an attribute of the element is added/deleted/modified.
attributeChangedCallback(attributeName) {

    ...

    this.dateElement.textContent = `Created on ${month} ${date.getDate()}`;
  }
}

All in all template strings offer a more readable and convenient syntax than string concatenation.

Formatting dates has been a bit of a mess (34 answers StackOverlfow!) but there is now a standard way of doing so with Intl.DateTimeFormat.

DateTimeFormat is not part of the ES2015 spec but the ECMA-402 spec however wide browser support is pretty recent so like ES2015 worth keeping in mind when building apps using standard cutting edge JavaScript.

main.js

// List of shortened month names.
StickyNote.MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov',
                    'Dec'];

...

// Fires when an attribute of the element is added/deleted/modified.
StickyNote.attributeChangedCallback = function(attributeName) {
  
    ...
   
    var month = StickyNote.MONTHS[date.getMonth()];
    this.dateElement.textContent = 'Created on ' + month + ' ' + date.getDate();
 }
};

Instead you can rely on Intl.DateTimeFormat to do the work:

main.es6.js

attributeChangedCallback(attributeName) {

    ...

    // Format the date
    let dateFormatterOptions = {day: 'numeric', month: 'short'};
    let shortDate = new Intl.DateTimeFormat("en-US", dateFormatterOptions).format(date);

    this.dateElement.textContent = `Created on ${shortDate}`;
  }
}

You can also remove StickyNote.MONTHS since we don't need it anymore.

ES6 now has a convenient way to pass any iterable objects (like arrays) and spread them as parameters of function calls. For instance the HTMLElement.classList#add takes strings as parameters. In ES5 you would have to iterate over your List/Array of classes to pass them to the function:

main.js

// Fires when an instance of the element is created.
StickyNote.createdCallback = function() {
  StickyNote.CLASSES.forEach(function(klass) {
    this.classList.add(klass);
  }.bind(this));

  ...

Note that in ES5 you could have also done the above in a one liner using the more obscure Function#apply function like this: this.classList.add.apply(this.classList, StickyNote.CLASSES);

In ES6 though you can now simply do:

main.es6.js

// Fires when an instance of the element is created.
createdCallback() {
  this.classList.add(...StickyNote.CLASSES);

  ...

Chrome and other evergreen browsers have good native support for ES2015. However to make sure your app has wide browser support you will need to transpile your ES2015 back to ES5.

Babel.js provides a very complete and reliable transpiler with tons of plugins and build tool integration.

If you want to quickly test your code in older browsers Babel.js provides an in-browser transpiler acting like a polyfill. All you need to do to use this is drop the following in your html pages' <head>:

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/6.0.20/browser.min.js"></script>

This is very convenient but does not offer the best performances and Babel.js advises developers not to use this in production.

Another way to go is to transpile files at build time (your IDE such as WebStorm might even has built-in dev time transpilation). Babel has a Command Line Interface. To install it just run:

npm install babel-cli babel-preset-es2015 --save-dev

This will add the Babel Command Line Interface to your project locally. You can now run Babel using:

node_modules/.bin/babel --presets es2015 scripts/main.es6.js -o scripts/main.es5.js -s true 

This creates a transpiled version of your scripts/main.es6.js as scripts/main.es5.js along with its source map. You can now just serve this file by changing the script tag in index.html:

index.html

...

<script src="scripts/main.es5.js"></script>
</body>
</html>

Another useful trick is to create an npm script as a shortcut to transpile your Javascript code. Add the following build script in your package.json file:

package.json

{

  ...

  "scripts": {
    "start": "serve --port 52587",
    "build": "babel --presets es2015 scripts/main.es6.js -o scripts/main.es5.js -s true"
  }
}

To transpile your JavaScript file you can now simply run:

npm run build

You can also have Babel watch for changes to your files and automatically transpile your file every time it changes. This makes it a good option for development. For this you'll need to add the -w flag. What you can do is add this command to your start npm script so that Babel auto-transpilation and your web server both work together:

package.json

{

  ...

  "scripts": {
    "start": "babel --presets es2015 scripts/main.es6.js -o scripts/main.es5.js -s true -w & serve --port 52587",
    "build": "babel --presets es2015 scripts/main.es6.js -o scripts/main.es5.js -s true"
  }
}

Now after running npm start your files will get automatically transpiled while being served from http://localhost:52587

Congrats! You're done! Feel free to compare what you have with: https://github.com/googlecodelabs/chrome-es2015/blob/master/scripts/main.es6_FINAL.js

You've converted a full app to the ES2015 syntax and kept your app compatible with all browsers!

What you've learned

Learn More

Have a look at other E2015 features that were not covered in this codelab:

Other