Buy

In the first JavaScript tutorial, we learned about objects. I mean, real objects: the kind you can instantiate by creating a constructor function, and then adding all the methods via the prototype. Objects look a lot different in PHP than in in JavaScript, in large part because PHP has classes and JavaScript doesn't. Well... that's a big fat lie! ES2015 introduces classes: true classes.

Creating a new class

As a PHP developer, you're going to love this... because the class structure looks nearly identical to PHP! If you want to create a Helper class... just say, class Helper {}:

203 lines web/assets/js/RepLogApp.js
'use strict';
(function(window, $, Routing, swal) {
... lines 4 - 172
/**
* A "private" object
*/
class Helper {
}
... lines 179 - 201
})(window, jQuery, Routing, swal);

That's it! With this syntax, the constructor is called, just, constructor. Move the old constructor function into the class and rename it: constructor. You can also remove the semicolon after the method, just like in PHP:

201 lines web/assets/js/RepLogApp.js
'use strict';
(function(window, $, Routing, swal) {
... lines 4 - 172
/**
* A "private" object
*/
class Helper {
constructor($wrapper) {
this.$wrapper = $wrapper;
}
... lines 180 - 198
}
})(window, jQuery, Routing, swal);

Moving everything else into the new class syntax is easy: remove $.extend(helper.prototype) and move all of the methods inside of the class:

201 lines web/assets/js/RepLogApp.js
'use strict';
(function(window, $, Routing, swal) {
... lines 4 - 172
/**
* A "private" object
*/
class Helper {
constructor($wrapper) {
this.$wrapper = $wrapper;
}
calculateTotalWeight() {
let totalWeight = 0;
this.$wrapper.find('tbody tr').each((index, element) => {
totalWeight += $(element).data('weight');
});
return totalWeight;
}
getTotalWeightString(maxWeight = 500) {
let weight = this.calculateTotalWeight();
if (weight > maxWeight) {
weight = maxWeight + '+';
}
return weight + ' lbs';
}
}
})(window, jQuery, Routing, swal);

And congratulations! We just created a new ES2015 class. Wasn't that nice?

To make things sweeter, it all works just like before: nothing is broken. And that's no accident: behind the scenes, JavaScript still follows the prototypical object oriented model. This new syntax is just a nice wrapper around it. It's great: we don't need to worry about the prototype, but ultimately, that is set behind the scenes.

Let's make the same change at the top with RepLogApp: class RepLogApp { and then move the old constructor function inside. But, make sure to spell that correctly! I'll indent everything and add the closing curly brace:

203 lines web/assets/js/RepLogApp.js
'use strict';
(function(window, $, Routing, swal) {
class RepLogApp {
constructor($wrapper) {
this.$wrapper = $wrapper;
this.helper = new Helper(this.$wrapper);
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',
this._selectors.newRepForm,
this.handleNewFormSubmit.bind(this)
);
}
}
... lines 28 - 201
})(window, jQuery, Routing, swal);

Cool! Now we all we need to do is move the methods inside!

Classes do not have Properties

Start by only moving the _selectors property. Paste it inside the class and... woh! PhpStorm is super angry:

Types are not supported by current JavaScript version

Rude! PhpStorm is trying to tell us that properties are not supported inside classes: only methods are allowed. That may seem weird - but it'll be more clear why in a minute. For now, change this to be a method: _getSelectors(). Add a return statement, and everything is happy:

207 lines web/assets/js/RepLogApp.js
'use strict';
(function(window, $, Routing, swal) {
class RepLogApp {
... lines 5 - 27
_getSelectors() {
return {
newRepForm: '.js-new-rep-log-form'
}
}
}
... lines 34 - 205
})(window, jQuery, Routing, swal);

Well, everything except for the couple of places where we reference the _selectors property. Yea, this._selectors, that's not going to work:

207 lines web/assets/js/RepLogApp.js
'use strict';
(function(window, $, Routing, swal) {
class RepLogApp {
constructor($wrapper) {
... lines 6 - 20
this.$wrapper.on(
... line 22
this._selectors.newRepForm,
... line 24
);
}
... lines 27 - 135
_mapErrorsToForm(errorData) {
... line 137
const $form = this.$wrapper.find(this._selectors.newRepForm);
... lines 139 - 152
},
_removeFormErrors() {
const $form = this.$wrapper.find(this._selectors.newRepForm);
... lines 157 - 158
},
_clearForm() {
... lines 162 - 163
const $form = this.$wrapper.find(this._selectors.newRepForm);
... line 165
},
... lines 167 - 176
});
... lines 178 - 205
})(window, jQuery, Routing, swal);

But don't fix it! Let's come back in a minute.

Right now, move the rest of the methods inside: just delete the } and the prototype line to do it. We can also remove the comma after each method:

203 lines web/assets/js/RepLogApp.js
'use strict';
(function(window, $, Routing, swal) {
class RepLogApp {
constructor($wrapper) {
... lines 6 - 25
}
_getSelectors() {
return {
newRepForm: '.js-new-rep-log-form'
}
}
loadRepLogs() {
... lines 35 - 41
}
updateTotalWeightLifted() {
... lines 45 - 47
}
handleRepLogDelete(e) {
... lines 51 - 63
}
_deleteRepLog($link) {
... lines 67 - 84
}
handleRowClick() {
... line 88
}
handleNewFormSubmit(e) {
... lines 92 - 106
}
_saveRepLog(data) {
... lines 110 - 129
}
_mapErrorsToForm(errorData) {
... lines 133 - 148
}
_removeFormErrors() {
... lines 152 - 154
}
_clearForm() {
... lines 158 - 161
}
_addRow(repLog) {
... lines 165 - 171
}
}
... lines 174 - 201
})(window, jQuery, Routing, swal);

Other than that, nothing needs to change.

Magic get Methods

Time to go back and fix this _getSelectors() problem. The easiest thing would be to update this._selectors to this._getSelectors(). But, there's a cooler way.

Rename the method back to _selectors(), and then add a "get space" in front of it:

206 lines web/assets/js/RepLogApp.js
'use strict';
(function(window, $, Routing, swal) {
class RepLogApp {
... lines 5 - 27
/**
* Call like this.selectors
*/
get _selectors() {
... lines 32 - 34
}
... lines 36 - 175
}
... lines 177 - 204
})(window, jQuery, Routing, swal);

Woh! Instantly, PhpStorm is happy: this is a valid syntax. And when you search for _selectors, PhpStorm is happy about those calls too!

This is the new "get" syntax: a special new feature from ES2015 that allows you to define a method that should be called whenever someone tries to access a property, like _selectors. There's of course also a "set" version of this, which would be called when someone tries to set the _selectors property.

So even though classes don't technically support properties, you can effectively create properties by using these get and set methods.

Oh, and btw, just to be clear: even though you can't define a property on a class, you can still set whatever properties you want on the object, after it's instantiated:

class CookieJar {
    constructor(cookies) {
        this.cookies = cookies;
    }
}

That hasn't changed.

Ok team! Try out our app! Refresh! It works! Wait, no, an error! Blast! It says:

RepLogApp is not defined

And the error is from our template: app/Resources/views/lift/index.html.twig:

83 lines app/Resources/views/lift/index.html.twig
... lines 1 - 53
{% block javascripts %}
... lines 55 - 59
<script>
$(document).ready(function() {
var $wrapper = $('.js-rep-log-table');
var repLogApp = new RepLogApp($wrapper);
});
</script>
... lines 66 - 81
{% endblock %}

Ah, this code is fine: the problem is that the RepLogApp class only lives within this self executing function:

206 lines web/assets/js/RepLogApp.js
'use strict';
(function(window, $, Routing, swal) {
class RepLogApp {
... lines 5 - 175
}
... lines 177 - 204
})(window, jQuery, Routing, swal);

It's the same problem we had in the first episode with scope.

Solve it in the same way: export the class to the global scope by saying window.RepLogApp = RepLogApp:

208 lines web/assets/js/RepLogApp.js
'use strict';
(function(window, $, Routing, swal) {
class RepLogApp {
... lines 5 - 175
}
... lines 177 - 205
window.RepLogApp = RepLogApp;
})(window, jQuery, Routing, swal);

Try it now! And life is good! So what else can we do with classes? What about static methods?

Leave a comment!

  • 2018-05-23 Nacer

    Diego Aguiar it's clear, thank you very much for the explanations :)

    Cheers!

  • 2018-05-23 Diego Aguiar

    In this case, I think it's ok because that function seems like you may want to use it from outside of the class. It just updates the total, but anyways, you can always add that "underscore" if you find any method not being used from outside (as soon as you need that, then you can refactor that method's name)

  • 2018-05-23 Nacer

    Hi @Diego sorry for the confusion between static and private, but i don"t understand yet the diffecrence bettwen _addRow and updateTotalWeightLifted, why updateTotalWeightLifted dosn't start with underscore like addRow for example ?

  • 2018-05-22 Diego Aguiar

    Hey Nacer

    That method cannot be static because it uses some properties of the class, and the "under score" convention for naming methods is for specifying which methods should be considered as "private", just like if you were declaring a "private function" in PHP

    Cheers!

  • 2018-05-22 Nacer

    Hello, can someone explain me why the function updateTotalWeightLifted is not a static and don't begin with underscore like _addRow for example, Thx :)