The road to Angular 2 - ES6 part 2
This post is the second part of a series on Angular 2, showing some chosen parts of our ebook. You may want to read the first part if you missed it!
We stopped when we were talking about classes, a new concept in JavaScript. Let’s see what we have in our second part!
Promises
Promises are not so new, and you might know them or use them already, as they were a big part of AngularJS 1.x. But since you will use them a lot in Angular 2, and even if you’re just using JS, I think it’s important to make a stop.
Promises aim to simplify asynchronous programming. Our JS code is full of async stuff, like AJAX requests, and usually we use callbacks to handle the result and the error. But it can get messy, with callbacks inside callbacks, and it makes the code hard to read and to maintain. Promises are much nicer than callbacks, as they flatten the code, and thus make it easier to understand. Let’s consider a simple use case, where we need to fetch a user, then its rights, then update a menu when we have these.
with callbacks:
getUser(login, function(user) {
getRights(user, function(rights) {
updateMenu(rights);
});
});
now, let’s compare it with promises:
getUser()
.then(function(user) {
return getRights(user);
})
.then(function(rights) {
updateMenu(rights);
})
I like this version, because it executes as you read it: I want to fetch a user, then get its rights, then update the menu.
As you can see, a promise is a ‘thenable’ object, which simply means it has a then
method. This method takes two arguments: one success callback and one reject callback. The promise has three states:
- pending: while the promise is not done, for example, our server call is not completed yet.
- fulfilled: when the promise is completed with success, for example, the server call returns an OK HTTP status.
- rejected: when the promise has failed, for example, the server returns a 404 status.
When the promise is fulfilled, then the success callback is called, with the result as an argument. If the promise is rejected, then the reject callback is called, with a rejected value or an error as the argument.
So, how do you create a promise? Pretty simple, there is a new class called Promise
, whose constructor expects a function with two parameters, resolve
and reject
.
let getUser = function() {
return new Promise(function(resolve, reject) {
// async stuff, like fetching users from server, returning a response
if (response.status === 200) {
resolve(response.data);
} else {
reject('No user');
}
});
};
Once you have created the promise, you can register callbacks, using the then
method. This method can receive two parameters, the two callbacks you want to call in case of success or in case of failure. Here we only pass a success callback, ignoring the potential error:
getUser()
.then(function(user) {
console.log(user);
})
Once the promise is resolved, the success callback (here simply logging the user on the console) will be called.
The cool part is that it flattens the code. For example, if your resolve callback is also returning a promise, you can write:
getUser()
.then(function(user) {
return getRights(user) // getRights is returning a promise
.then(function(rights) {
return updateMenu(rights);
});
})
but more beautifully:
getUser()
.then(function(user) {
return getRights(user); // getRights is returning a promise
})
.then(function(rights) {
return updateMenu(rights);
})
Another interesting thing is the error handling, as you can use one handler per promise, or one for all the chain.
One per promise:
getUser()
.then(function(user) {
return getRights(user);
}, function(error) {
console.log(error); // will be called if getUser fails
return Promise.reject(error);
})
.then(function(rights) {
return updateMenu(rights);
}, function(error) {
console.log(error); // will be called if getRights fails
return Promise.reject(error);
})
One for the chain:
getUser()
.then(function(user) {
return getRights(user);
})
.then(function(rights) {
return updateMenu(rights);
})
.catch(function(error) {
console.log(error); // will be called if getUser or getRights fails
})
You should seriously look into Promises, because they are going to be the new way to write APIs, and every library will use them. Even the standard ones: the new Fetch API does for example.
Arrow functions
One thing I like a lot in ES6 is the new arrow function syntax, using the ‘fat arrow’ operator (=>
). It is SO useful for callbacks and anonymous functions!
Let’s take our previous example with promises:
getUser()
.then(function(user) {
return getRights(user); // getRights is returning a promise
})
.then(function(rights) {
return updateMenu(rights);
})
can be written with arrow functions like this:
getUser()
.then(user => getRights(user))
.then(rights => updateMenu(rights))
How cool is it? THAT cool!
Note that the return is also implicit if there is no block: no need to write user => return getRights(user)
. But if we did have a block, we would need the explicit return:
getUser()
.then(user => {
console.log(user);
return getRights(user);
})
.then(rights => updateMenu(rights))
And it has a special trick, a great power over normal functions: the this
stays lexically bounded, which means that these functions don’t have a new this
as other functions do. Let’s take an example, where you are iterating over an array with the map
function to find the max.
In ES5:
var maxFinder = {
max: 0,
find: function(numbers) {
// let's iterate
numbers.forEach(
function(element) {
// if the element is greater, set it as the max
if (element > this.max) {
this.max = element;
}
});
}
};
maxFinder.find([2, 3, 4]);
// log the result
console.log(maxFinder.max);
You would expect this to work, but it doesn’t.
If you have a good eye, you may have noticed that the forEach
in the find
function uses this
,
but the this
is not bound to an object.
So this.max
is not the max
of the maxFinder
object…
Of course you can fix it easily, using an alias:
var maxFinder = {
max: 0,
find: function(numbers) {
var self = this;
numbers.forEach(
function(element) {
if (element > self.max) {
self.max = element;
}
});
}
};
maxFinder.find([2, 3, 4]);
// log the result
console.log(maxFinder.max);
or binding the this
:
var maxFinder = {
max: 0,
find: function(numbers) {
numbers.forEach(
function(element) {
if (element > this.max) {
this.max = element;
}
}.bind(this));
}
};
maxFinder.find([2, 3, 4]);
// log the result
console.log(maxFinder.max);
or even passing it as a second parameter of the forEach
function (as it was designed for):
var maxFinder = {
max: 0,
find: function(numbers) {
numbers.forEach(
function(element) {
if (element > this.max) {
this.max = element;
}
}, this);
}
};
maxFinder.find([2, 3, 4]);
// log the result
console.log(maxFinder.max);
But there is now an even more elegant solution with the arrow function syntax:
let maxFinder = {
max: 0,
find: function(numbers) {
numbers.forEach(element => {
if (element > this.max) {
this.max = element;
}
});
}
};
maxFinder.find([2, 3, 4]);
// log the result
console.log(maxFinder.max);
That makes the arrow functions the perfect candidates for anonymous functions in callbacks!
Sets and Maps
This is a short one: you now have proper collections in ES6. Yay \o/! We used to have dictionaries filling the role of a map, but we can now use the class Map
:
let cedric = { id: 1, name: 'Cedric' };
let users = new Map();
users.set(cedric.id, cedric); // adds a user
console.log(users.has(cedric.id)); // true
console.log(users.size); // 1
users.delete(cedric.id); // removes the user
We also have Set
:
let cedric = { id: 1, name: 'Cedric' };
let users = new Set();
users.add(cedric); // adds a user
console.log(users.has(cedric)); // true
console.log(users.size); // 1
users.delete(cedric); // removes the user
You can iterate over a collection, with the new syntax for ... of
:
for (let user of users) {
console.log(user.name);
}
You’ll see that the for ... of
syntax is the one the Angular team chose to iterate over a collection in a template.
Template literals
Composing strings has always been painful in JavaScript, as we usually have to use concatenation:
let fullname = 'Miss ' + firstname + ' ' + lastname;
Template literals are a new small feature, where you have to use backticks (`
) instead of quotes or simple quotes, and you have a basic templating system, with multiline support:
let fullname = `Miss ${firstname} ${lastname}`;
The multiline support is especially great when your are writing HTML strings, as we will do for our Angular components:
let template = `<div>
<h1>Hello</h1>
</div>`;
Modules
A standard way to organize functions in namespaces and to dynamically load code in JS has always been lacking. NodeJS has been one of the leaders in this, with a thriving ecosystem of modules using the CommonJS convention. On the browser side, there is also the AMD standard, used by RequireJS. But none of these were a real standard, thus leading to endless discussions on what’s best.
ES6 aims to create a syntax using the best from both worlds, without caring about the actual implementation. The TC39 committee wanted to have a nice and easy syntax (that’s arguably CommonJS’s strong suit), but to support asynchronous loading (like AMD), and a few goodies like the possibility to be statically analyzed by tools and support cyclic dependencies nicely. The new syntax handles how you export and import things to and from modules.
This module thing is really important in Angular 2, as pretty much everything is defined in modules, that you have to import when you want to use them. Let’s say I want to expose a function to bet on a specific pony in a race and a function to start the race.
In racesServices.js:
export function bet(race, pony) {
// ...
}
export function start(race) {
// ...
}
As you can see, this is fairly easy: the new keyword export
does a straightforward job and exports the two functions.
Now, let’s say one of our application components needs to call these functions:
In racesService.js
import { bet, start } from 'races_service';
bet(race, pony1);
start(race);
That’s what is called a named export. Here we are importing the two functions, and we have to specify the filename containing these functions - here ‘racesServices’. Of course, you can import only one method if you need, you can even give it an alias:
import { start as startRace } from 'races_service';
startRace(race);
And if you need to import all the methods from the module, you can use a wildcard ‘*’.
As you would do with other languages, use the wildcard with care, only when you really want all the functions, or most of them. As this will be analyzed by our IDEs, we will see auto-import soon and that will free us from the bother to import the right things.
With a wildcard, you have to use an alias, and I kind of like it, because it makes the rest of the code clearer:
import * as racesService from 'races_service';
racesService.bet(race, pony1);
racesService.start(race);
If your module exposes only one function or value or class, you don’t have to use named export, and you can leverage the default keyword. It works great for classes for example:
// pony.js
export default class Pony { }
// racesService.js
import Pony from 'pony';
Notice the lack of curly braces to import a default. You can import it with the alias you want, but to be consistent, it’s better to call the import with the module name (except if you have multiple modules with the same name of course, then you can chose an alias that allows to distinguish them). And of course, you can mix default export with named ones, but obviously with only one default per module.
In Angular 2, you’re going to use a lot of these imports in your app. Each component and service will be a class, generally isolated in their own file and exported, and then imported when needed in other components.
That ends our gentle introduction to ES6. We skipped some other parts, but if you’re comfortable with this chapter, you will have no problem writing your apps in ES6. If you want to have a deeper understanding of this, I highly recommend Exploring JS by Axel Rauschmayer or Understanding ES6 from Nicholas C. Zakas … Both ebooks can be read online for free, but don’t forget to buy it to support their authors, they have done a great work! Actually I’ve re-read Speaking JS, Axel’s previous book, and I again learned a few things, so if you want to refresh your JS skills, I definitely recommend it!
Stay tuned for another episode of The road to Angular 2!