As we prepare for support of ES2015 in Meteor, it's important that we understand what supporting ES2015 will look like and how it will impact our applications. In this snippet, we'll take a quick look at common Meteor patterns rewritten using ES2015. While this won't be a deep dive into ES2015 itself, it should be enough to help you understand how to bring existing and new applications up to date with the latest version of JavaScript.
In order to illustrate usage of ES2016, we'll be using some of the code from the Building Complex Forms recipe.
When we say "Template Logic" in Meteor, we're referring to the JavaScript that's paired with our HTML/Spacebars templates. First, let's look at an example of some template logic written in ES5. This is taken from the template logic paired with the order form in the Building Complex Forms recipe.
/client/templates/public/order.js
Template.order.onCreated( function() {
this.subscribe( 'order' );
this.currentOrder = new ReactiveDict();
if ( Meteor.user() ) {
this.currentOrder.set( "type", "My Pizzas" );
} else {
this.currentOrder.set( "type", "Popular Pizzas" );
}
this.currentOrder.set( "pizza", { "name": "Pick a pizza!", "price": 0 } );
});
Template.order.onRendered( function() {
var template = Template.instance();
$( "#place-order" ).validate({
rules: {
customPizzaName: {
required: true
},
name: {
required: true
},
telephone: {
required: true
},
streetAddress: {
required: true
},
city: {
required: true
},
state: {
required: true
},
zipCode: {
required: true
},
emailAddress: {
required: true,
email: true
},
password: {
required: true,
minlength: 6
}
},
submitHandler: function() {
var orderData = template.currentOrder;
type = orderData.get( "type" ),
pizza = orderData.get( "pizza" ),
order = {};
if ( Meteor.user() ) {
order.customer = Meteor.userId();
} else {
order.customer = {
name: template.find( "[name='name']" ).value,
telephone: template.find( "[name='telephone']" ).value,
streetAddress: template.find( "[name='streetAddress']" ).value,
secondaryAddress: template.find( "[name='secondaryAddress']" ).value,
city: template.find( "[name='city']" ).value,
state: template.find( "[name='state']" ).value,
zipCode: template.find( "[name='zipCode']" ).value
}
order.credentials = {
email: template.find( "[name='emailAddress']").value,
password: template.find( "[name='password']").value
}
}
if ( type === "Custom Pizza" ) {
var meatToppings = [],
nonMeatToppings = [];
$( "[name='meatTopping']:checked" ).each( function( index, element ) {
meatToppings.push( element.value );
});
$( "[name='nonMeatTopping']:checked" ).each( function( index, element ) {
nonMeatToppings.push( element.value );
});
var customPizza = {
name: template.find( "[name='customPizzaName']" ).value,
size: template.find( "[name='size'] option:selected" ).value,
crust: template.find( "[name='crust'] option:selected" ).value,
sauce: template.find( "[name='sauce'] option:selected" ).value,
toppings: {
meats: meatToppings,
nonMeats: nonMeatToppings
},
custom: true,
price: 1000
};
}
if ( pizza.name === "Pick a pizza!" ) {
Bert.alert( "Make sure to pick a pizza!", "warning" );
} else {
order.pizza = pizza._id ? pizza._id : customPizza;
Meteor.call( "placeOrder", order, function( error, response ) {
if ( error ) {
Bert.alert( error.reason, "danger" );
} else {
Bert.alert( "Order submitted!", "success" );
if ( order.credentials ) {
Meteor.loginWithPassword( order.credentials.email, order.credentials.password );
}
Router.go( "/profile" );
}
});
}
}
});
});
Template.order.helpers({
customer: function() {
if ( Meteor.userId() ) {
var getCustomer = Customers.findOne( { "userId": Meteor.userId() } );
} else {
var getCustomer = {};
}
if ( getCustomer ) {
getCustomer.context = "order";
return getCustomer;
}
},
order: function() {
var currentOrder = Template.instance().currentOrder,
type = currentOrder.get( "type" ),
pizza = currentOrder.get( "pizza" ),
price = currentOrder.get( "price");
if ( type !== "Custom Pizza" ) {
var getPizza = pizza._id ? Pizza.findOne( { "_id": pizza._id } ) : pizza;
} else {
var getPizza = {
name: "Build your custom pizza up above!",
price: 1000
}
}
if ( getPizza ) {
return {
type: pizza.name !== "Pick a pizza!" ? type : null,
pizza: getPizza,
price: getPizza.price
}
}
}
});
Template.order.events({
'click .nav-tabs li': function( event, template ) {
var orderType = $( event.target ).closest( "li" ).data( "pizza-type" );
template.currentOrder.set( "type", orderType );
if ( orderType !== "Custom Pizza" ) {
template.currentOrder.set( "pizza", { "name": "Pick a pizza!", "price": 0 } );
} else {
template.currentOrder.set( "pizza", { "name": "Build your custom pizza up above!", "price": 0 } );
}
},
'click .pizza': function( event, template ) {
template.currentOrder.set( "pizza", this );
if ( this.custom ) {
template.currentOrder.set( "type", "My Pizzas" );
} else {
template.currentOrder.set( "type", "Popular Pizzas" );
}
},
'submit form': function( event ) {
event.preventDefault();
}
});
That's a pretty big chunk of code! Let's refactor this using ES2015 to see how we can simplify it a bit. We'll step through each template method : onCreated
, onRendered
, helpers
, and events
.
Template.order.onCreated in ES2015
Template.order.onCreated( () => {
this.subscribe( 'order' );
this.currentOrder = new ReactiveDict();
if ( Meteor.user() ) {
this.currentOrder.set( "type", "My Pizzas" );
} else {
this.currentOrder.set( "type", "Popular Pizzas" );
}
this.currentOrder.set( "pizza", { "name": "Pick a pizza!", "price": 0 } );
});
Yep, that's it! Pretty simple, right? Here, the only thing we've changed is the function declaration to use the Arrow syntax. Everything else stays the same.
Template.order.onRendered in ES2015
Template.order.onRendered( () => {
let template = Template.instance();
$( "#place-order" ).validate({
rules: {
customPizzaName: {
required: true
},
name: {
required: true
},
telephone: {
required: true
},
streetAddress: {
required: true
},
city: {
required: true
},
state: {
required: true
},
zipCode: {
required: true
},
emailAddress: {
required: true,
email: true
},
password: {
required: true,
minlength: 6
}
},
submitHandler() {
let orderData = template.currentOrder;
type = orderData.get( "type" ),
pizza = orderData.get( "pizza" ),
order = {};
if ( Meteor.user() ) {
order.customer = Meteor.userId();
} else {
order.customer = {
name: template.find( "[name='name']" ).value,
telephone: template.find( "[name='telephone']" ).value,
streetAddress: template.find( "[name='streetAddress']" ).value,
secondaryAddress: template.find( "[name='secondaryAddress']" ).value,
city: template.find( "[name='city']" ).value,
state: template.find( "[name='state']" ).value,
zipCode: template.find( "[name='zipCode']" ).value
}
order.credentials = {
email: template.find( "[name='emailAddress']").value,
password: template.find( "[name='password']").value
}
}
if ( type === "Custom Pizza" ) {
let meatToppings = [],
nonMeatToppings = [];
$( "[name='meatTopping']:checked" ).each( ( index, element ) => {
meatToppings.push( element.value );
});
$( "[name='nonMeatTopping']:checked" ).each( ( index, element ) => {
nonMeatToppings.push( element.value );
});
let customPizza = {
name: template.find( "[name='customPizzaName']" ).value,
size: template.find( "[name='size'] option:selected" ).value,
crust: template.find( "[name='crust'] option:selected" ).value,
sauce: template.find( "[name='sauce'] option:selected" ).value,
toppings: {
meats: meatToppings,
nonMeats: nonMeatToppings
},
custom: true,
price: 1000
};
}
if ( pizza.name === "Pick a pizza!" ) {
Bert.alert( "Make sure to pick a pizza!", "warning" );
} else {
order.pizza = pizza._id ? pizza._id : customPizza;
function placeOrder( order ) {
return new Promise( ( resolve, reject ) => {
Meteor.call( "placeOrder", order, ( error, response ) => {
if ( error ) {
reject( error.reason );
} else {
resolve( "Order submitted!" );
}
})
})
}
placeOrder( order ).then( message => {
Bert.alert( message, "success" );
if ( order.credentials ) {
Meteor.loginWithPassword( order.credentials.email, order.credentials.password );
}
Router.go( "/profile" );
}).catch( error => {
Bert.alert( error, "danger" );
})
}
}
});
});
Still pretty straightforward, but this one requires a bit of explanation in some parts. First, notice that we've swapped all usage of var
with let
. let
is the counterpart to const
in ES2015. With let
, we create a variable that can be reassigned later. In contrast, with const
, we create a constant variable, meaning it cannot be changed later. Because we're creating a number of variables that will change at some point, it's safe to use let
here.
Notice, too, that we've gone through and replaced all instances of function(){}
with the new arrow syntax like () => {}
. These are mostly harmless and really just help to save a few keystrokes (we'll see some situations later on where these may not be appropriate). The functions we're calling here are pretty beefy, so they all rely on the Statement body syntax of Arrow functions.
Another thing to point out is how we've defined our submitHandler
function. Notice that instead of defining it like submitHandler: function(){}
, we've shortened it down to submitHandler() {}
. What gives? This is part of the new enhanced object literals feature in ES2015. Inside of object literals, now, we can define methods using the functionName() {}
syntax. Again, just another "keystroke saver," but handy nonetheless.
The real behemoth in this is the refactor of our method call to placeOrder
on the server to use Promises. While not entirely necessary here—it's just cool to show off how it works—we can see a few neat things going on that are good to know for later. Let's break that piece out on its own and then step through it.
Promise Wrapped Methods in ES2015
function placeOrder( order ) {
return new Promise( ( resolve, reject ) => {
Meteor.call( "placeOrder", order, ( error, response ) => {
if ( error ) {
reject( error.reason );
} else {
resolve( "Order submitted!" );
}
});
});
}
placeOrder( order ).then( message => {
Bert.alert( message, "success" );
if ( order.credentials ) {
Meteor.loginWithPassword( order.credentials.email, order.credentials.password );
}
Router.go( "/profile" );
}).catch( error => {
Bert.alert( error, "danger" );
});
So what the heck is happening here? The purpose of a Promise is to allow us to call asynchronous funtions in a synchronous manner. This means that instead of having to nest a bunch of if/else
statements, we can have a simple syntax for saying "do this, and if that works, do this, and then if anything fails, do this." The point being that we get a much cleaner syntax for defining calls to asynchronous functions that are expected to succeed or fail at some point in the future.
In order to define a new Promise, we need to create a new function that accepts any arguments we'd like to pass to the function we're wrapping in a Promise. Let that sink in. In the case of our example, we're trying to pass an object order
to our placeOrder
method. So, we make sure to pass order
as an argument to our new placeOrder
function that will wrap our Promise. Hang in there!
Next, we define our Promise new Promise
returning it from our placeOrder
function and passing two arguments resolve
and reject
. These arguments each represent an "event" of sorts in relation to our promise. When our function works as expected, we resolve it, when it fails, we reject it. Inside of our Promise definition, we can see a standard Meteor.call( "placeOrder" )
, taking the order
argument we passed (containing our object with order information).
Finally, we pass a regular callback function checking for an error
argument. But wait! Notice that instead of just calling the code we might inside of our if/else
block, we instead return either reject()
—invoking it as a function and passing our error message—or, we return resolve()
, also invoking it as a function but passing a success message.
This is all gibberish until we move down a few lines to where we actually call our placeOrder
function. At this point, we've created a new promise. To determine what happens next, we rely on chaining a .then()
method. Pay attention here. We handle what happens when our function is called successfully first—that resolve()
part—and at the end, we chain on a .catch()
method to handle any errors that occur.
We've only showcased the usage of one then()
method here, but in practice, we can chain multiple then()
s if we'd like. This allows us to avoid nesting a bunch of callbacks while passing along the result from each successive function call. The only thing to note is that your catch()
method will always come last.
In our example, if our Promise resolves as expected, we pass the success messsage "Order submitted!"
from within in our Promise to our call to Bert.alert()
. The string we passed in the resolve, then, is equivalent to the message argument we pass to our .then()
method. Notice that the same thing applies with our .catch()
method on the end. That error
argument being passed to Bert.alert()
maps to the error.reason
we passed to the reject()
method inside of our Promise. Wow!
While we have added a bit of code here, what we pick up in return is a much cleaner—and arguably, predictable—syntax. We can see the actual steps that our function goes through, both in good and bad scenarios. Neat!
That was a hell of a detour! Let's jump back up to refactor the helpers
for our order
template into some spiffy ES2015 code. Don't worry, that Promise thing was as hardcore as we get.
Template.order.helpers in ES2015
Template.order.helpers({
customer() {
if ( Meteor.userId() ) {
let getCustomer = Customers.findOne( { "userId": Meteor.userId() } );
} else {
let getCustomer = {};
}
if ( getCustomer ) {
getCustomer.context = "order";
return getCustomer;
}
},
order() {
let currentOrder = Template.instance().currentOrder,
type = currentOrder.get( "type" ),
pizza = currentOrder.get( "pizza" ),
price = currentOrder.get( "price");
if ( type !== "Custom Pizza" ) {
let getPizza = pizza._id ? Pizza.findOne( { "_id": pizza._id } ) : pizza;
} else {
let getPizza = {
name: "Build your custom pizza up above!",
price: 1000
}
}
if ( getPizza ) {
return {
type: pizza.name !== "Pick a pizza!" ? type : null,
pizza: getPizza,
price: getPizza.price
}
}
}
});
Easy peasy. Two changes to point out here. We've swapped all of our var
s with let
s and then we've updated each of our helper function definitions to use the new enhanced object literal syntax of defining methods. So, instead of customer: function(){}
, we get customer() {}
. Underwhelming for everyone but our keyboards.
Last up, event maps! Let's take a peek.
Template.order.events in ES2015
Template.order.events({
'click .nav-tabs li': ( event, template ) => {
let orderType = $( event.target ).closest( "li" ).data( "pizza-type" );
template.currentOrder.set( "type", orderType );
if ( orderType !== "Custom Pizza" ) {
template.currentOrder.set( "pizza", { "name": "Pick a pizza!", "price": 0 } );
} else {
template.currentOrder.set( "pizza", { "name": "Build your custom pizza up above!", "price": 0 } );
}
},
'click .pizza': function( event, template ) {
template.currentOrder.set( "pizza", this );
if ( this.custom ) {
template.currentOrder.set( "type", "My Pizzas" );
} else {
template.currentOrder.set( "type", "Popular Pizzas" );
}
},
'submit form': ( event ) => {
event.preventDefault();
}
});
Just two changes here: swapping functions to use the Arrow syntax and changing any var
s to let
s. Wait a second! Notice that our second event is not using the Arrow syntax. Why is that? Well, when we use the Arrow syntax, this
is equal to a lexical this
. What that means is that this
is not equal to the current function, but rather, to the parent context, or Window
. Deep breaths. This is pretty confusing. This Stack Overflow answer helped me to understand it a bit better:
Lexical Scoping defines how variable names are resolved in nested functions: inner functions contain the scope of parent functions even if the parent function has returned.
Making some sense? So, with a plain function(){}
, this
is equal to function this
is being referenced in. With Arrow syntax, this
is equal to the outer parent functions.
Don't panic, but we're about to see something similar with Publications.
It's far too long to post here, but if you're curious, you can console.log( this );
, alternating between the regular function() {}
style and the Arrow syntax () => {}
. The first version will return this
as expected (the current data context of the template this event is being called on), while the latter will return the Window
object in all its glory.
At this point, you should notice a recurring theme: the bulk of the code we'll write in Meteor using ES2015 is not scary. A lot of the changes we're making are minor and mostly cosmetic. As another example, let's look at setting up a pubication.
Publications in ES2015
Meteor.publish( 'pizzaProfile', function() {
let user = this.userId;
let data = [
Pizza.find( { $or: [ { "custom": true, "ownerId": user }, { "custom": false } ] } ),
Customers.find( { "userId": user } ),
Orders.find( { "userId": user } )
];
if ( data ) {
return data;
}
return this.ready();
});
Just when you thought it was all blue skies! Nothing to panic about here, but notice that we're not using the Arrow syntax to define our function here. Again, this is because of the lexical scope of Arrow functions. Because we're referencing this.userId
, we need access to the server object that we normally have access to in publications. To do it, we have to call a plain function() {}
. Otherwise, this
will be equal to the entirety of Meteor
(the main object that holds all of the properties, methods, etc for Meteor). Yikes!
Okay. That's the last heart attack. From here on out everything is pretty easy going.
Routes are pretty basic. For good measure:
Defining Routes in ES2015 (and Iron Router)
Router.route( 'profile', {
path: '/profile',
template: 'pizzaProfile',
onBeforeAction() {
Session.set( 'currentRoute', 'profile' );
this.next();
}
});
It won't get much crazier than that. For clarity sake, if this route were defined in Flow Router, we may see something like this:
Defining Routes in ES2015 (and Flow Router)
FlowRouter.route( '/profile', {
action() {
BlazeLayout.render( 'applicationLayout', { main: 'pizzaProfile' } );
},
name: 'pizzaProfile'
});
Again, the only thing really changing here is that we're swapping method declarations from method: function() {}
to just method() {}
.
Speaking of methods, what do Meteor Methods look like in ES2015?
Another example taken from Building Complex Forms, this one is the counterpart method being called by the method call we refactored to use Promises earlier.
Meteor.methods({
placeOrder( order ) {
check( order, Object );
const handleOrder = {
createUser( credentials ) {
try {
let userId = Accounts.createUser( credentials );
return userId;
} catch( exception ) {
return exception;
}
},
createCustomer( customer, userId ) {
customer.userId = userId;
let customerId = Customers.insert( customer );
return customerId;
},
createPizza( pizza, userId ) {
pizza.ownerId = userId;
let pizzaId = Pizza.insert( pizza );
return pizzaId;
},
createOrder( userId, pizzaId ) {
let orderId = Orders.insert({
userId: userId,
pizzaId: pizzaId,
date: ( new Date() )
});
return orderId;
}
}
try {
let userId = order.credentials ? handleOrder.createUser( order.credentials ) : order.customer,
customerId = order.customer.name ? handleOrder.createCustomer( order.customer, userId ) : null,
pizzaId = order.pizza.custom ? handleOrder.createPizza( order.pizza, userId ) : order.pizza;
orderId = handleOrder.createOrder( userId, pizzaId );
return orderId;
} catch( exception ) {
return exception;
}
}
});
Straightforward? Because our method is being defined inside of an object literal, we can use the new method() {}
syntax to kickoff our method definition. Inside, we make use of const
to define an object handleOrder
which is then assigned a series of methods (we use the enhanced object literal syntax again to define each). The thing to call out here is our usage of const
. Because this object contains a series of methods that we do not want to change, we use const
to block any accidental reassignment of the handleOrder
object later.
To make that clear, if beneath the handleOrder
definition we called some code like handleOrder = "bye bye methods"
, we'd get an error. If we used let
to define handleOrder
, then, it would be reassigned to "bye bye methods"
and we'd see our castle come tumbling down. This is one of the scenarios you will want to watch out for when trying to figure out whether to use let
or const
.
That's it for Methods! Not much to change aside from the code you've defined in them.
To better handle loops, we get a new type of loop for( var example of examples ) {}
. You may be thinking to yourself..."we already have this?" Close! Right now we have access to for( var example in examples ) {}
. Spot the difference? One is using in
and the other is using of
. What's the difference?
When using the in
version of the for
loop, var example
is equal to the current property name. In the of
version of the for
loop, var example
is equal to the current property value. Let's look at another example from our Building Complex Forms recipe.
Using for In loops in ES5
createPizzas = function() {
var pizzas = [
{
"name": "Classic Supreme",
"crust": "Thin",
"toppings": {
"meats": [ 'Sausage', 'Pepperoni' ],
"nonMeats": [ 'Green Peppers', 'Mushrooms', 'Black Olives', 'Onions' ]
},
"sauce": "Tomato",
"size": 14,
"price": 1000,
"custom": false
},
{
"name": "Chicago",
"crust": "Deep Dish",
"toppings": {
"meats": [ 'Pepperoni' ],
"nonMeats": [ 'Banana Peppers', 'Green Peppers', 'Mushrooms', 'Black Olives', 'Onions' ]
},
"sauce": "Robust Tomato",
"size": 12,
"price": 1500,
"custom": false
},
{
"name": "Classic Pepperoni",
"crust": "Regular",
"toppings": {
"meats": [ 'Pepperoni' ],
"nonMeats": []
},
"sauce": "Tomato",
"size": 12,
"price": 1000,
"custom": false
}
];
var pizzaCount = Pizza.find().count();
if ( pizzaCount < 1 ) {
for ( var pizza in pizzas ) {
Pizza.insert( pizzas[ pizza ] );
}
}
};
And now, let's refactor this to use ES2015 and the of
syntax for our for
loop:
Using for of loops in ES5
createPizzas = () => {
let pizzas = [
[...]
];
let pizzaCount = Pizza.find().count();
if ( pizzaCount < 1 ) {
for ( let pizza of pizzas ) {
Pizza.insert( pizza );
}
}
};
The difference here is subtle. Can you spot it? Before, when we were looping our pizzas and inserting one into the Pizza
collection, we had to call Pizza.insert( pizzas[ pizza ] );
. This translated to us selecting the current pizza being looped over in the pizzas
array. With the new of
syntax, we can instead just grab the pizza we want to insert directly and call Pizza.insert( pizza );
. Remember, this works because of
tells our loop to assign the current property value being looped, not the property name.
Quite a bit! Because our focus was just on rewriting common Meteor patterns in ES2015, we've left out a few features. Before we part ways, though, there are two features we should call out as they're pretty handy for Meteor apps: classes and modules.
While not necessary to define basic Meteor code, a feature of ES2015 that we can start using to organize our own code is classes. Classes are just a syntactic wrapper around prototyping in JavaScript. They're designed to give a bit of structure to the process of defining methods on and creating instances of objects. Here's a quick example:
Example Using Classes
class Taco {
constructor( name, toppings ) {
this.name = name;
this.toppings = toppings.slice( 0, toppings.length - 1 ).join( ", " ) + `, and ${ toppings.pop() }`;
}
makeTaco() {
console.log( `Nothing like a ${this.name} taco with ${this.toppings}!` );
}
}
fiestaTaco = new Taco( "Fiesta", [ 'Beef', 'Cheese', 'Lettuce', 'Tomatoes', 'Gravy' ] );
While we probably won't be making a lot of tacos in our Meteor applications—especially with gravy as a topping—this example shows how a class can work to give our code some structure. Notice, a class behaves just like a regular prototype object. Classes are great for code that we'll need to call again and again but with different properties.
In this example, we define our Class and then add a method called constructor()
at the top (a convention introduced in the Class specification). Inside, we assign some values for the current instance of our Class using this
. We assign two properties to our Class instance: name
and toppings
. Name is pretty straightforward: it just gets assigned to whatever we pass in the first argument when creating an instance of our class new Taco( "This Argument" );
.
Next, we get a little tricky and assign our Class instance's this.toppings
value equal to a string we build using the toppings
argument that's passed along when creating an instance of our class new Taco( "Name", [ 'These', 'Are', 'What', 'We', 'Want' ];
. To build that string, we use another feature of ES2015, Template Strings, to allow us to do something called String Interpolation. In normal people words, interpolate means to evaluate the code passed within ${}
and print its value in the string at that point (where ${someExpression}
acts as a placeholder).
In our example, then, after we've split off the last topping in our array, we append it using a Template String. Notice that unlike a normal string where we'd use quotes ""
, to make use of Template Strings we have to define our string using backtics ``
. How about them apples.
Once our Constructor()
is defined, we add a method to our class called makeTaco()
that simply logs a string to the console. Again, we make use of Template Strings to get this working. Lastly, we assign an instance of our Taco
class to a global variable called fiestaTaco
. If we open up our browser console then—assuming this code is defined on the client—we'd get something like this:
Browser Console
fiestaTaco.makeTaco();
> "Nothing like a Fiesta taco with Beef, Cheese, Lettuce, Tomatoes, and Gravy!"
We can create as many instances of Taco
as we want now, passing any name or toppings that we wish. Pretty neat.
Another feature that's not quite supported in Meteor is the usage of Modules. Modules allow us to break our code up into individual files (e.g. defining a class in its own file) and then import those files—or code—into other files. Because Meteor's current design automatically imports all of our files for us, this functionality will not work. There are plans, however, to enable this in a future version of Meteor. Fingers crossed!
As features start to emerge for supporting different features of ES2015 in Meteor (and as I understand more and more about it), this snippet will be updated. Stay tuned!
- ES2015 is just the new version of JavaScript. It only introduces syntax changes and new features, not a radical new version of the language.
- When using the Arrow syntax with functions, be careful with lexical scope returning the parent scope instead of what you intended.
- Start using ES2015! Full support will come in Meteor 1.2 but we can start playing with it now.