Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Legit JavaScript Classes

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

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 {}:

'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:

'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:

'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:

'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:

'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:

'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:

'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:

'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:

... 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:

'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:

'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!

5
Login or Register to join the conversation
Miloud B. Avatar
Miloud B. Avatar Miloud B. | posted 5 years ago

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

Reply

Hey Miloud

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!

Reply
Miloud B. Avatar

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 ?

Reply

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)

Reply
Miloud B. Avatar
Miloud B. Avatar Miloud B. | MolloKhan | posted 5 years ago | edited

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

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses Symfony 3. But, since this is a JavaScript tutorial, all the concepts work fine in newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "symfony/symfony": "3.2.*", // v3.2.14
        "twig/twig": "2.10.*", // v2.10.0
        "doctrine/orm": "^2.5", // v2.7.1
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.2
        "symfony/swiftmailer-bundle": "^2.3", // v2.4.2
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.3.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.19
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "~2.0@dev", // dev-master
        "doctrine/doctrine-fixtures-bundle": "~2.3", // v2.4.1
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.2.1
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "friendsofsymfony/jsrouting-bundle": "^1.6" // 1.6.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.0" // v3.2.2
    }
}
userVoice