Buy

Babel: Transliterating to Old JavaScript

Time to use Babel! How? At your terminal, type

./node_modules/.bin/babel

Tip

On some systems, you may need to type:

node ./node_modules/.bin/babel

That is the path to the executable for Babel. Next, point to our source file: web/assets/js/RepLogApp.js and then pass -o and the path to where the final, compiled, output file should live: web/assets/dist/RepLogApp.js.

Before you run that, go into web/assets, and create that new dist/ directory. Now, hold your breath and... run that command!

./node_modules/.bin/babel web/assets/js/RepLogApp.js -o web/assets/dist/RepLogApp.js

And boom! Suddenly, we have a new RepLogApp.js file.

Before we look at it, go into index.html.twig and update the script tag to point to the new dist version of RepLogApp.js that Babel just created:

67 lines app/Resources/views/lift/index.html.twig
... lines 1 - 53
{% block javascripts %}
... lines 55 - 57
<script src="{{ asset('assets/dist/RepLogApp.js') }}"></script>
... lines 59 - 65
{% endblock %}

Ok, refresh! It still works!

So what did Babel do? What are the differences between those two files? Let's find out! Open the new file:

218 lines web/assets/dist/RepLogApp.js
'use strict';
(function (window, $, Routing, swal) {
let HelperInstances = new WeakMap();
class RepLogApp {
constructor($wrapper) {
this.$wrapper = $wrapper;
this.repLogs = new Set();
HelperInstances.set(this, new Helper(this.repLogs));
this.loadRepLogs();
this.$wrapper.on('click', '.js-delete-rep-log', this.handleRepLogDelete.bind(this));
this.$wrapper.on('click', 'tbody tr', this.handleRowClick.bind(this));
this.$wrapper.on('submit', RepLogApp._selectors.newRepForm, this.handleNewFormSubmit.bind(this));
}
... lines 20 - 165
}
/**
* A "private" object
*/
class Helper {
constructor(repLogSet) {
this.repLogSet = repLogSet;
}
... lines 175 - 197
}
const rowTemplate = repLog => `
<tr data-weight="${repLog.totalWeightLifted}">
<td>${repLog.itemLabel}</td>
<td>${repLog.reps}</td>
<td>${repLog.totalWeightLifted}</td>
<td>
<a href="#"
class="js-delete-rep-log"
data-url="${repLog.links._self}"
>
<span class="fa fa-trash"></span>
</a>
</td>
</tr>
`;
window.RepLogApp = RepLogApp;
})(window, jQuery, Routing, swal);

Hmm, it actually doesn't look any different. And, that's right! To prove it, use the diff utility to compare the files:

diff -u web/assets/js/RepLogApp.js web/assets/dist/RepLogApp.js

Wait, so there are some differences... but they're superficial: just a few space differences here and there. Babel did not actually convert the code to the old JavaScript format! We can still see the arrow functions!

Here's the reason. As crazy as it sounds, by default, Babel does... nothing! Babel is called a transpiler, which other than being a cool word, means that it reads source code and converts it to other source code. In this case, it parses JavaScript, makes some changes to it, and outputs JavaScript. Except that... out-of-the-box, Babel doesn't actually make any changes!

Adding babel-preset-env

We need a little bit of configuration to tell Babel to do the ES2015 to ES5 transformation. In other words, to turn our new JavaScript into old JavaScript.

And they mention it right on the installation page! At the bottom, they tell you that you probably need something called babel-preset-env. In Babel language, a preset is a transformation. If we want Babel to make the ES2015 transformation, we need to install a preset that does that. The env preset is one that does that. And there are other presets, like CoffeeScript, ActionScript and one for ReactJS that we'll cover in the future!

Let's install the preset with yarn:

yarn add babel-preset-env --dev

Perfect! To tell Babel to use that preset, at the root of the project, create a .babelrc file. Babel will automatically read this configuration file, as long as we execute Babel from this directory. Inside, add "presets": ["env"]:

4 lines .babelrc
{
"presets": ["env"]
}

This comes straight from the docs. And... we're done!

Try the command again! Run that diff command now:

./node_modules/.bin/babel web/assets/js/RepLogApp.js -o web/assets/dist/RepLogApp.js
diff -u web/assets/js/RepLogApp.js web/assets/dist/RepLogApp.js

Woh! Now there are big differences! In fact, it looks like almost every line changed. Let's go look at the new RepLogApp.js file in dist/ - it's really interesting.

Cool! First, Babel adds a few utility functions at the top:

337 lines web/assets/dist/RepLogApp.js
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
(function (window, $, Routing, swal) {
... lines 8 - 335
})(window, jQuery, Routing, swal);

Below, instead of using the new class syntax, it calls one of those functions - _createClass() - which helps to mimic that new functionality:

337 lines web/assets/dist/RepLogApp.js
... lines 1 - 6
(function (window, $, Routing, swal) {
... lines 8 - 10
var RepLogApp = function () {
function RepLogApp($wrapper) {
_classCallCheck(this, RepLogApp);
this.$wrapper = $wrapper;
this.repLogs = new Set();
HelperInstances.set(this, new Helper(this.repLogs));
this.loadRepLogs();
this.$wrapper.on('click', '.js-delete-rep-log', this.handleRepLogDelete.bind(this));
this.$wrapper.on('click', 'tbody tr', this.handleRowClick.bind(this));
this.$wrapper.on('submit', RepLogApp._selectors.newRepForm, this.handleNewFormSubmit.bind(this));
}
/**
* Call like this.selectors
*/
_createClass(RepLogApp, [{
key: 'loadRepLogs',
value: function loadRepLogs() {
var _this = this;
$.ajax({
url: Routing.generate('rep_log_list')
}).then(function (data) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = data.items[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var repLog = _step.value;
_this._addRow(repLog);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
});
}
... lines 66 - 252
}], [{
key: '_selectors',
get: function get() {
return {
newRepForm: '.js-new-rep-log-form'
};
}
}]);
return RepLogApp;
}();
... lines 264 - 335
})(window, jQuery, Routing, swal);

Our arrow functions are also gone, replaced with classic anonymous functions.

There's a lot of cool, but complex stuff happening here. And fortunately, we don't need to worry about any of this! It just works! Now, even an older browser can enjoy our awesome, new code.

Tip

The purpose of the babel-preset-env is for you to configure exactly what versions of what browsers you need to support. It then takes care of converting everything necessary for those browsers.

Babel and the Polyfill

But wait... it did not change our WeakMap!

337 lines web/assets/dist/RepLogApp.js
... lines 1 - 6
(function (window, $, Routing, swal) {
var HelperInstances = new WeakMap();
... lines 10 - 335
})(window, jQuery, Routing, swal);

But... isn't that only available in ES2015? Yep! Babel's job is to convert all the new language constructs and syntaxes to the old version. But if there are new objects or functions, it leaves those. Instead, you should use something called a polyfill. Specifically, babel-polyfill. This is another JavaScript library that adds missing functionality, like WeakMap, if it doesn't exist in whatever browser is running our code.

We actually did something just like this in the first episode. Remember when we were playing with the Promise object? Guess what? That object is only available in ES2015. To prevent browser issues, we used a polyfill.

To use this Polyfill correctly, we need to go a little bit further and learn about Webpack. That's the topic of our next tutorial... where we're going to take a huge step forward with how we write JavaScript. With webpack, we'll be able to do cool stuff like importing JavaScript files from inside of each other:

// actually imports code from helper.js!
import myHelperFunctions from './helper';

myHelperFunctions.now();

Heck, you can even import CSS from inside of JavaScript. It's bananas.

Ok guys! I hope you learned tons about ES2015/ES6/Harmony/Larry! You can already start using it by using Babel. Or, if your users all have brand-new browsers, then lucky you!

All right guys, I'll seeya next time.

Leave a comment!