ES6 for Drupal Developers: ES6 Modules, Classes, and Promises

  • 9 minute read

Some of the most important new features in ES6 originate from existing solutions for problems shared by a large swath of JavaScript applications. This is particularly true of ES6 modules, a new system of class-based inheritance, and promises. Prototypal inheritance can be difficult for those new to JavaScript. And at some point, modules and promises have both been present in various JavaScript libraries, but until now, they were impossible to implement natively.

In the previous installment of this blog series, we looked at some of the most conspicuous aspects of ES6, such as arrow functions and concise methods, in addition to smaller syntactic features like template literals and computed property names. In this final installment of the “ES6 for Drupal Developers” series, we’ll dig into ES6 modules, classes, and promises, all essential elements that are emblematic of the new way to write JavaScript.

ES6 modules and named exports

The most popular way to organize code in multiple files in JavaScript is through ES6 modules. At the moment, however, using JavaScript modules on the server requires a transpiler such as Babel, because Node.js uses CommonJS require syntax and relies on the V8 engine, which lacks support for modules. In browsers, meanwhile, native ES6 module support is poor and under active development.

// users.js
var people = ["Dries", "Earl", "Fabien", "Greg", "Hal"];
export default people;
// say-hello.js
import people from 'people';
var hello = {
  sayHello: () => {
    people.forEach((name) => {
      console.log('Hi ' + name + '!');
    });
  }
};

You can name your exports, which are imported under the same nomenclature.

// users.js
var people = ["Dries", "Earl", "Fabien", "Greg", "Hal"];
var otherPeople = ["Matt", "Nathan", "Oliver"];
export { people, otherPeople };
// say-hello.js
import { people, otherPeople } from 'people';

You can also rename any named imports.

import {
  people as users,
  otherPeople as friends
} from 'people';

You can also import everything from a module using namespace imports.

import * as users from 'people';

ES6 classes

The new class keyword delineates a block where members of a function’s prototype are defined. It’s important to note, however, that class-based inheritance in ES6 is merely syntactic sugar on top of the existing prototypal paradigm in ES5.

To start, take this ES5 example using prototypal inheritance.

// ES5
function Baz(x, y) {
  this.a = x;
  this.b = y;
}

Baz.prototype.addVals = function () {
  return this.a + this.b;
};

In ES6, you can now use constructor syntax which is familiar to those working with other object-oriented paradigms.

// ES6
class Baz {
  constructor(x, y) {
    this.a = x;
    this.b = y;
  }
  addVals() {
    return this.a + this.b;
  }
}

Now you can instantiate the class and use it just as you would a prototype in ES5.

var myBaz = new Baz(3, 4);
myBaz.a; // 3
myBaz.b; // 4
myBaz.addVals(); // 7

extends and super

Class inheritance in JavaScript can now be done using approaches more familiar to those who have not worked with prototypal inheritance.

class Calculation {
  constructor(x, y) {
    this.a = x;
    this.b = y;
  }
}

In no surprise to PHP developers, extends allows for class inheritance. Here, super refers to the parent constructor.

class Add extends Calculation {
  constructor(x, y) {
    super(x, y);
  }
  addVals() {
    return this.a + this.b;
  }
}
class Multiply extends Calculation {
  constructor(x, y) {
    super(x, y);
  }
  multiplyVals() {
    return this.a * this.b;
  }
}

You can invoke methods on super to gain access to parent methods.

class Calculation {
  // ...
  describe() {
    return "Calculation";
  }
}
class Add extends Calculation {
  constructor(x, y) {
    super(x, y);
    // ... 
  }
  describe() {
    return "Addition " + super.describe();
  }
}

Static methods and new.target

Using the static keyword with a method definition means that that method is only accessible from that class’s function object, not to any of its prototypes.

class Add extends Calculation {
  constructor(x, y) {
    super(x, y);
    // ... 
  }
  static describe() {
    return "Addition " + super.describe();
  }
}

Add.describe(); // "Addition Calculation"
var foo = new Add();
foo.describe(); // undefined

A somewhat strange-looking “meta property” lets you know which constructor the new keyword specifically invoked. If new.target gives undefined, you know the new keyword was not used.

class Calculation {
  constructor(x, y) {
    console.log("Calculation constructor called from ", new.target.name);
  }
}
class Add extends Calculation {
  constructor(x, y) {
    super(x, y);
    console.log("Add constructor called from ", new.target.name);
  }
  describe() {
    console.log("Describe method called from ", new.target);
  }
}

var foo = new Calculation(); // Calculation constructor called from Calculation
var bar = new Add(); // Calculation constructor called from Add
// Add constructor called from Add
bar.describe();
// Describe method called from undefined (no constructor invoked)

ES6 promises

You can think of promises as event listeners, on which you can register to inform you that something is done (which will only happen once). I prefer to think of promises as an eventual return value with no set arrival time.

You can use the Promise() constructor to instantiate a new promise.

var promise = new Promise(
  function pr(resolve, reject) {
    // Eventually call resolve() or reject(). 
  }
);

There are several important things to remember with regards to promise usage.

  • Calling reject() means the promise is also rejected. The argument of reject() is the motive for rejection.
  • Calling resolve() without a value or a value not provided by promise, then the promise is fulfilled.
  • Calling resolve() and passing another promise means that the surrounding promise will adopt the fulfillment or rejection of the promise within.

Here’s a quick example with a fictional AJAX request.

function ajax(url) {
  return new Promise( function pr(resolve, reject) {
    // Perform AJAX request, then call resolve() or reject().
  });
}
ajax("http://data.backend.url").then(
  function fulfilled(response) {
    // Success
  },
  function rejected(motive) {
    // Error
  }
);

Epilogue: ES6 and Drupal

ES6 can already be leveraged in your own Drupal build (consider the es6-promise library, which is already incorporated into the Acquia Lightning distribution). However, you’ll need to think about your toolchain in terms of development dependencies such as Babel and how your development workflow ought to evolve as a result.

But what about Drupal 8 core development? Is there a place for ES6 in Drupal’s future?

Much of Drupal’s JavaScript can be refactored to incorporate ES6 features. However, this means that JavaScript development in Drupal now requires Babel and that patches must run through a build process before they can be submitted. Though these development workflow changes add complexity, they are necessary to ensure that Drupal is prepared for the imminent days when ES6 is natively supported on both server and client.

Especially given our discussion over a year ago about potentially moving Drupal closer to a more JavaScript-heavy future and current efforts to bring ES6 development into Drupal’s JavaScript, I encourage you to think critically about the future of JavaScript in Drupal as we explore new approaches and contribute your own thoughts and patches. If we so wished, it could be that the next Drupal front end will be entirely driven by JavaScript — and ES6.

New to this series? Check out the first part, second part, and third part of the "ES6 for Drupal developers" blog series. This blog series is a heavily updated and improved version of “Introduction to ES6”, a session delivered at SANDCamp 2016. Special thanks to Matt Grill for providing feedback during the writing process.