Immediately Invoked Function Expression!

Our code is growing up! And to keep going, it's really time to move our RepLogApp into its own external JavaScript file. For now, let's keep this real simple: inside the web/ directory - which is the public document root for the project - and in assets/, I'll create a new js/ directory. Then, create a new file: RepLogApp.js. Copy all of our RepLogApp object and paste it here:

53 lines web/assets/js/RepLogApp.js
var RepLogApp = {
initialize: function ($wrapper) {
this.$wrapper = $wrapper;
this.$wrapper.find('.js-delete-rep-log').on(
'click',
this.handleRepLogDelete.bind(this)
);
this.$wrapper.find('tbody tr').on(
'click',
this.handleRowClick.bind(this)
);
},
updateTotalWeightLifted: function () {
var totalWeight = 0;
this.$wrapper.find('tbody tr').each(function () {
totalWeight += $(this).data('weight');
});
this.$wrapper.find('.js-total-weight').html(totalWeight);
},
handleRepLogDelete: function (e) {
e.preventDefault();
var $link = $(e.currentTarget);
$link.addClass('text-danger');
$link.find('.fa')
.removeClass('fa-trash')
.addClass('fa-spinner')
.addClass('fa-spin');
var deleteUrl = $link.data('url');
var $row = $link.closest('tr');
var self = this;
$.ajax({
url: deleteUrl,
method: 'DELETE',
success: function () {
$row.fadeOut('normal', function () {
$(this).remove();
self.updateTotalWeightLifted();
});
}
});
},
handleRowClick: function () {
console.log('row clicked!');
}
};

Add a good old-fashioned script tag to bring this in:

77 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
{{ parent() }}
<script src="{{ asset('assets/js/RepLogApp.js') }}"></script>
<script>
$(document).ready(function() {
var $table = $('.js-rep-log-table');
RepLogApp.initialize($table);
});
</script>
{% endblock %}

If you don't normally use Symfony, ignore the asset() function: it doesn't do anything special.

To make sure we didn't mess anything up, refresh! Let's add a few items to our list. Then, delete one. It works!

Private Functions in JavaScript

One of the advantages of having objects in PHP is the possibility of having private functions and properties. But, that doesn't exist in JavaScript: everything is publicly accessible! That means that anyone could call any of these functions, even if we don't intend for them to be used outside of the object.

That's not the end of the world, but it is a bummer! Fortunately, by being clever, we can create private functions and variables. You just need to think differently than you would in PHP.

Creating a Faux-Private Method

First, create a function at the bottom of this object called _calculateTotalWeight:

59 lines web/assets/js/RepLogApp.js
var RepLogApp = {
... lines 2 - 49
_calculateTotalWeight: function() {
... lines 51 - 56
}
};

Its job will be to handle the total weight calculation logic that's currently inside updateTotalWeightLifted:

59 lines web/assets/js/RepLogApp.js
var RepLogApp = {
... lines 2 - 49
_calculateTotalWeight: function() {
var totalWeight = 0;
this.$wrapper.find('tbody tr').each(function () {
totalWeight += $(this).data('weight');
});
return totalWeight;
}
};

We're making this change purely for organization: my intention is that we will only use this method from inside of this object. In other words, ideally, calculateTotalWeight would be private!

But since everything is public in JavaScript, a common standard is to prefix methods that should be treated as private with an underscore. It's a nice convention, but it doesn't enforce anything. Anybody could still call this from outside of the object.

Back in updateTotalWeightLifted, call it: this._calculateTotalWeight():

59 lines web/assets/js/RepLogApp.js
var RepLogApp = {
... lines 2 - 13
updateTotalWeightLifted: function () {
this.$wrapper.find('.js-total-weight').html(
this._calculateTotalWeight()
);
},
... lines 19 - 57
};

Creating a Private Object

So how could we make this truly private? Well, you can't make methods or properties in an object private. BUT, you can make variables private, by taking advantage of variable scope. What I mean is, if I have access to the RepLogApp object, then I can call any methods on it. But if I didn't have access to this, or some other object, then of course I wouldn't be able to call any methods on it. I know that sounds weird, so let's do it!

At the bottom of this file, create another object called: var Helper = {}:

69 lines web/assets/js/RepLogApp.js
... lines 1 - 54
var Helper = {
... lines 56 - 67
};

Commonly, we'll organize our code so that each file has just one object, like in PHP. But eventually, this variable won't be public - it's just a helper meant to be used only inside of this file.

I'll even add some documentation: this is private, not meant to be called from outside!

69 lines web/assets/js/RepLogApp.js
... lines 1 - 51
/**
* A "private" object
*/
var Helper = {
... lines 56 - 67
};

Just like before, give this an initialize, function with a $wrapper argument. And then say: this.$wrapper = $wrapper:

69 lines web/assets/js/RepLogApp.js
... lines 1 - 54
var Helper = {
initialize: function ($wrapper) {
this.$wrapper = $wrapper;
},
... lines 59 - 67
};

Move the calculateTotalWeight() function into this object, but take off the underscore:

69 lines web/assets/js/RepLogApp.js
... lines 1 - 54
var Helper = {
... lines 56 - 59
calculateTotalWeight: function() {
var totalWeight = 0;
this.$wrapper.find('tbody tr').each(function () {
totalWeight += $(this).data('weight');
});
return totalWeight;
}
};

Technically, if you have access to the Helper variable, then you're allowed to call calculateTotalWeight. Again, that whole _ thing is just a convention.

Back in our original object, let's set this up: call Helper.initialize() and pass it $wrapper:

69 lines web/assets/js/RepLogApp.js
var RepLogApp = {
initialize: function ($wrapper) {
this.$wrapper = $wrapper;
Helper.initialize(this.$wrapper);
... lines 5 - 13
},
... lines 15 - 49
};
... lines 51 - 69

Down below, call this: Helper.calculateTotalWeight():

69 lines web/assets/js/RepLogApp.js
var RepLogApp = {
initialize: function ($wrapper) {
this.$wrapper = $wrapper;
Helper.initialize(this.$wrapper);
... lines 5 - 13
},
updateTotalWeightLifted: function () {
this.$wrapper.find('.js-total-weight').html(
Helper.calculateTotalWeight()
);
},
... lines 20 - 49
};
... lines 51 - 69

Double-check that everything still works: refresh! It does!

But, this Helper object is still public. What I mean is, we still have access to it outside of this file. If we try to console.log(Helper) from our template, it works just fine:

78 lines app/Resources/views/lift/index.html.twig
... lines 1 - 64
{% block javascripts %}
... lines 66 - 69
<script>
console.log(Helper);
... lines 72 - 75
</script>
{% endblock %}

What I really want is the ability for me to choose which variables I want to make available to the outside world - like RepLogApp - and which I don't, like Helper.

The Self-Executing Function

The way you do that is with - dun dun dun - an immediately invoked function expression. Also known by its friends as a self-executing function. Basically, that means we'll wrap all of our code inside a function... that calls itself. It's weird, but check it out: (function() {, then indent everything. At the bottom, add the }) and then ():

71 lines web/assets/js/RepLogApp.js
(function() {
var RepLogApp = {
... lines 3 - 50
};
... lines 52 - 55
var Helper = {
... lines 57 - 68
};
})();

What?

There are two things to check out. First, all we're doing is creating a function: it starts on top, and ends at the bottom with the }. But by adding the (), we are immediately executing that function. We're creating a function and then calling it!

Why on earth would we do this? Because! Variable scope in JavaScript is function based. When you create a variable with var, it's only accessible from inside of the function where you created it. If you have functions inside of that function, they have access to it too, but ultimately, that function is its home.

Before, when we weren't inside of any function, our two variables effectively became global: we could access them from anywhere. But now that we're inside of a function, the RepLogApp and Helper variables are only accessible from inside of this self-executing function.

This means that when we refresh, we get Helper is not defined. We just made the Helper variable private!

Unfortunately... we also made our RepLogApp variable private, which means the code in our template will not work. We still need to somehow make RepLogApp available publicly, but not Helper. How? By taking advantage of the magical window object.

Leave a comment!