Javascript’s ES6 upgrade has been coming for a long time and brings a lot of really great features. Since the spec was finalized late last year, we’ve jumped in with both feet and begun using ES6 in considerable parts of our new web applications.
The internet already has plenty of tutorials covering specific ES6 features and how to use them, but most of them are shown in isolation and there’s not enough examples of how to actually pick up those code snippets and get them working in a real web application.
So, this blog post is a random collection of tips, tricks & styles that we’ve begun using in real-world web applications. In no particular order…
Running ES6 and ES5 Side-by-side
Practically speaking, you are very unlikely to be able to develop an application wholly in ES6. If nothing else, most of your plugins are still in ES5. Specifically, we’re talking about how modules are injected into the system. Typically, this is done using an AMD dependency injection system like RequireJS, but ES6 has a snazzy new module syntax which is designed to replace this.
The practical challenge is getting these two technologies to run side by side. Or, from our new ES6 point of view – how do we import a non-ES6 module? The answer is SystemJS
.
Although it’s technically not, we like to think of SystemJS as a kind of wrapper for RequireJS and ES6 modules. It basically reads what the structure of your file is and:
- if it contains ES6 module syntax, it assumes it is an ES6 module
- otherwise, loads as if it is a RequireJS module
One gotcha that took us about an hour to work out was that you need to kick it all off using a call to System.import
. In retrospect, this is pretty obvious – I mean, something has to tell your browser how to start loading external dependencies. So basically, it all ties together like this:
logon.es6
logon.es6 is your snazzy new logon module, written entirely in ES6. It uses the new module and class syntax and your girlfriend thinks it’s really good. Note that it has two dependencies – the first is lib, which you wrote yourself in ES5 and the second is a third-party plugin (in this case jQuery) which may or may not have any AMD- or module-syntax embedded.
import $ from 'jquery';
import lib from 'lib';
export default class Logon {
AttemptLogOn(){
let params = {
username: $('#TxtUserName').val(),
password: $('#TxtPassword').val()
};
lib.CallService('/secure/login', params);
}
};
lib.js
lib.js is that old library file which you’ve built up over the last few years. It is written in ES5 and full of helpful utility methods which you really can’t be bothered upgrading to ES6. It uses RequireJS syntax to declare its dependencies at the top of the file.
require(['anotherdependency'], function(anotherDep) {
return function(){
var CallService = function(relativeUrl, params){
};
return {
CallService: CallService
}
};
});
Logon.html
Your regular HTML page. It uses System.import
to get the ball rolling:
<script src="systemjs.js"></script>
<input type="text" id="TxtUserName"/>
<input type="password" id="TxtPassword"/>
<script>
System.import('logon.es6').then(function(l){
var log = new l();
log.AttemptLogin();
});
One more thing – you’ll probably need to use System.config
call to tell it how your files are organized, etc.:
System.config({
baseURL: '/scripts/',
paths: {
'Views/*': '/views/*.js'
},
map: {
"jquery": "lib/jquery",
"jqueryui": "lib/jquery-ui.min"
},
meta: {
"lib/jquery-ui-min": {
deps: ['lib/jquery']
}
}
});
Writing a Re-usable Base Class using ES6 Inheritance
Along with modules, this is the feature we most appreciate in ES6 – a tidy way to create a re-usable base class for our controllers. See, here is how our projects are typically laid out:
- The application is divided into heaps and heaps of modules, like ‘logon’, ‘view chart’, ‘render menu’, etc.
- Each of these modules has a JavaScript controller class which is bound to a view (using RivetsJS – we have an in-depth tutorial here)
- There is a lot of common code which is repeated in our controllers, such as:
- an
Init()
method to kick things off - a property called ‘
model
’ where we store the data for our view/controller - a reference to the view, in case we have to do something nasty like use jQuery to animate an element
Using ES6, we’ve now been able to create a tidy little BaseController
class which encapsulates this once:
basecontroller.es6
Our Base Controller class is written exactly like a regular ES6 class…
import $ from 'jquery';
import lib from 'lib';
export default class BaseController{
constructor(m) {
this.IsLoading = false;
this.model = m;
if (this.model !== null) this.view = $('#' + this.model.UniqueID);
else this.view = $('<div></div>');
this.Init();
}
Init(){
}
UpdateModel(newModel){
$.extend(this.model, newModel);
}
CallJSON(url, params){
var p = new Promise((success, fail) => {
this.view.addClass('loading');
this.IsLoading = true;
lib.CallService(url, params).then(result => {
this.view.removeClass('loading');
this.IsLoading = false;
success(result);
}, err => {
console.log("Error", err);
this.view.removeClass('loading');
this.IsLoading = false;
fail();
});
});
return p;
}
}
logon.es6
Our re-written logon file may now look like this:
import BaseController from 'basecontroller';
export default class LogonControl extends BaseController {
AttemptSignIn(){
alert('Your current PersonID is ' + this.model.PersonID);
let params = {
username: this.view.find('#TxtUserName').val(),
password: this.view.find('#TxtPassword').val()
};
this.CallJSON('signin', params).then((newModel) => {
this.UpdateModel(newModel);
alert('Your new PersonID is ' + this.model.PersonID);
});
}
};
And of course, you kick it all off by instantiating logon.es6 with a model in the constructor (note that the constructor is in the BaseController
class, and accepts one parameter):
System.import('logon').then((l) => {
let model = {
PersonID: 0
};
var log = new l(model);
log.Init();
});
Using traceur to Make Your ES6 Code Backwards Compatible
Currently, most browsers only support a tiny subset of the ES6 standards and we doubt that we could rely wholly on them coming up to speed for at least another 12 months, likely much longer. So it becomes necessary to run a transpiler which converts your ES6 code back into ES5.
As far as we can tell, the most complete transpiler out there is Google’s Traceur.
There are two ways of doing this, the lazy way and the proper way. The lazy way is to just include a script file in the <head/>
of your application, but we’re not even going to show a demo of that here because it is short-sighted. (If you’re asking, the thing we hate most is not that the transpiling is done in real-time in the browser, but the fact that you have to decorate your <script/>
tags with type=”module”
).
The better way to do this is to setup a task which runs traceur against your ES6 files at compile time and then point your browser at the generated ES5 files. For this, we’ve used the new Gulp integration supported by Visual Studio 2015. This is not the place to give a Gulp/VS tutorial, but once you’ve got your head around it, here is how we at Blackball do our transpiling:
gulpfile.js
var gulp = require('gulp');
var watch = require('gulp-watch');
var traceur = require('gulp-traceur');
var rename = require("gulp-rename");
function onError(error) {
console.log("ERROR: " + error.toString());
this.emit('end');
}
gulp.task('watchjs', function () {
gulp.watch('**/*.es6', ['compiletraceur']);
});
gulp.task('compiletraceur', function () {
return gulp.src('scripts/**/*.es6')
.pipe(traceur())
.on('error', onError)
.pipe(rename(function (path) {
path.extname = ".js";
}))
.pipe(gulp.dest('scripts/'));
});
Read it slowly and it kind of makes sense.
One problem with traceur is that your browser is running code which you didn’t write, so error logs do not match your ES6 files one-to-one. This is surprisingly okay though – even though the structure of your files differs, then lines that cause errors are generally pretty similar and practically speaking we haven’t had any problems understanding what part of our ES6 code the error pertains to.
Summing Up
Whether you like it or not, you’re all going to be coding in ES6 in the next five years so you better get on board. Due to the lack of support (tooling, blogs/forums, browsers….), it is not really practical to use it today, however if you like to play with new toys, then hopefully this article will save you a few hours…