Organizing with Objects!

Ok, this all looks pretty good... except that our code is just a bunch of functions and callback functions! Come on people, if this were PHP code, we would be using classes and objects. Let's hold our JavaScript to that same standard: let's use objects.

Creating an Object

How do you create an object? There are a few ways, but for now, it's as simple as var RepLogApp = {}:

117 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
... lines 66 - 67
<script>
var RepLogApp = {
... lines 70 - 108
};
... lines 110 - 114
</script>
{% endblock %}

Yep, that's an object. Yea, I know, it's just an associative array but an associative array is an object in JavaScript. And its keys become the properties and methods on the object. See, JavaScript doesn't have classes like PHP, only objects. Well, that's not entirely true, but we'll save that for a future tutorial.

Adding a Method

Anyways, let's give our object a new method: an initialize key set to a function(). We'll call this when the page loads, and its job will be to attach all the event handlers for all the events that we need on our table. Give it a $wrapper argument:

117 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
... lines 66 - 67
<script>
var RepLogApp = {
initialize: function($wrapper) {
... lines 71 - 80
},
... lines 82 - 108
};
... lines 110 - 114
</script>
{% endblock %}

Setting a Property

Before we do anything else, set that $wrapper argument onto a property: this.$wrapper = $wrapper:

117 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
... lines 66 - 67
<script>
var RepLogApp = {
initialize: function($wrapper) {
this.$wrapper = $wrapper;
... lines 72 - 80
},
... lines 82 - 108
};
... lines 110 - 114
</script>
{% endblock %}

Yep, we just dynamically added a new property. This is the second time we've seen the this variable in JavaScript. And this time, it's more familiar: it refers to this object.

Next, copy our first listener registration code, but change $table to this.$wrapper. And instead of using a big ugly anonymous function, let's make this event call a new method on our object: this.handleRepLogDelete:

117 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
... lines 66 - 67
<script>
var RepLogApp = {
initialize: function($wrapper) {
this.$wrapper = $wrapper;
this.$wrapper.find('.js-delete-rep-log').on(
'click',
this.handleRepLogDelete
);
... lines 77 - 80
},
... lines 82 - 108
};
... lines 110 - 114
</script>
{% endblock %}

We'll add that in a moment.

Repeat this for the other event listener: copy the registration line, change $table to this.$wrapper, and then on click, call this.handleRowClick:

117 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
... lines 66 - 67
<script>
var RepLogApp = {
initialize: function($wrapper) {
this.$wrapper = $wrapper;
... lines 72 - 76
this.$wrapper.find('tbody tr').on(
'click',
this.handleRowClick
);
},
... lines 82 - 108
};
... lines 110 - 114
</script>
{% endblock %}

I already like it!

After initialize, create these methods! Add a key called, handleRepLogDelete set to a new function:

117 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
... lines 66 - 67
<script>
var RepLogApp = {
... lines 70 - 82
handleRepLogDelete: function(e) {
... lines 84 - 103
},
... lines 105 - 108
};
... lines 110 - 114
</script>
{% endblock %}

Then go copy all of our original handler code, delete it, and put it here:

117 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
... lines 66 - 67
<script>
var RepLogApp = {
... lines 70 - 82
handleRepLogDelete: function(e) {
e.preventDefault();
$(this).addClass('text-danger');
$(this).find('.fa')
.removeClass('fa-trash')
.addClass('fa-spinner')
.addClass('fa-spin');
var deleteUrl = $(this).data('url');
var $row = $(this).closest('tr');
var $totalWeightContainer = $table.find('.js-total-weight');
var newWeight = $totalWeightContainer.html() - $row.data('weight');
$.ajax({
url: deleteUrl,
method: 'DELETE',
success: function() {
$row.fadeOut();
$totalWeightContainer.html(newWeight);
}
});
},
... lines 105 - 108
};
... lines 110 - 114
</script>
{% endblock %}

Make sure you have the, e argument exactly like before.

Do the same thing for our other method: handleRowClick set to a function() {}:

117 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
... lines 66 - 67
<script>
var RepLogApp = {
... lines 70 - 105
handleRowClick: function() {
... line 107
}
};
... lines 110 - 114
</script>
{% endblock %}

I'm not using the, e argument, so I don't need to add it. Copy the console.log() line, delete it, and put it here:

117 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
... lines 66 - 67
<script>
var RepLogApp = {
... lines 70 - 105
handleRowClick: function() {
console.log('row clicked!');
}
};
... lines 110 - 114
</script>
{% endblock %}

Don't Call your Handler Function: Pass It

There's one teenie detail I want you to notice: when we specify the event callback, this.handleRepLogDelete - we're not executing it:

117 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
... lines 66 - 67
<script>
var RepLogApp = {
initialize: function($wrapper) {
... lines 71 - 72
this.$wrapper.find('.js-delete-rep-log').on(
... line 74
this.handleRepLogDelete
);
this.$wrapper.find('tbody tr').on(
... line 78
this.handleRowClick
);
},
... lines 82 - 108
};
... lines 110 - 114
</script>
{% endblock %}

I mean, there are no () on the end of it. Nope, we're simply passing the function as a reference to the on() function. If you forget and add (), things will get crazy.

Initializing (not Instantiating) the Object

Back in the (document).ready(), our job is really simple: find the $table and then pass it to RepLogApp.initialize():

117 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
... lines 66 - 67
<script>
var RepLogApp = {
... lines 70 - 108
};
$(document).ready(function() {
var $table = $('.js-rep-log-table');
RepLogApp.initialize($table);
});
</script>
{% endblock %}

The cool thing about this approach is that now we have an entire object who's job is to work inside of this.$wrapper.

Ok, let's try this! Go back and refresh! Hit delete! Ah, it fails!

Variable $table is not defined.

The problem is inside of handleRepLogDelete. Ah, cool, this makes total sense. Before, we had a $table variable defined above the function. That's gone, but no problem! Just use this.$wrapper:

117 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
... lines 66 - 67
<script>
var RepLogApp = {
... lines 70 - 82
handleRepLogDelete: function(e) {
... lines 84 - 93
var $totalWeightContainer = this.$wrapper.find('.js-total-weight');
... lines 95 - 103
},
... lines 105 - 108
};
... lines 110 - 114
</script>
{% endblock %}

You can already see how handy an object can be.

Ok, go back and refresh again. Open up the console, click delete and... whoa! That doesn't work either! The errors is on the exact same line. What's going on here? It says:

Cannot read property 'find' of undefined

How can this.$wrapper be undefined? Let's find out.

Leave a comment!

  • 2017-02-10 Max

    Thx! I'm going to hit that one soon ;)

  • 2017-02-10 Victor Bocharsky

    Yo Max,

    Thanks a lot! And that's a really good question. Well, yes and no :) Actually, JS has ability to add constructors, but not in that form we see it in PHP classes. Just go further and you will see how to add a constructor... but if you can't wait - take a look at: https://knpuniversity.com/s...

    Cheers!

  • 2017-02-10 Max

    Hey guys!

    As always amazing work - highly instructive and fun to code along!

    There is no equivalent of a __construct() function in js, right? As well call initialize explicitly...