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.
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 {}
:
; | |
(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:
; | |
(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:
; | |
(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:
; | |
(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:
; | |
(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:
; | |
(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:
; | |
(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:
; | |
(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:
; | |
(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
:
; | |
(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?
Hello, can someone explain me why the function updateTotalWeightLifted is not a static and don't begin with underscore like _addRow for example, Thx :)