Buy Access to Course
08.

Legit JavaScript Classes

Share this awesome video!

|

Keep on Learning!

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?