If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOur 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:
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:
... 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!
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.
First, create a function at the bottom of this object called _calculateTotalWeight
:
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
:
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()
:
var RepLogApp = { | |
... lines 2 - 13 | |
updateTotalWeightLifted: function () { | |
this.$wrapper.find('.js-total-weight').html( | |
this._calculateTotalWeight() | |
); | |
}, | |
... lines 19 - 57 | |
}; |
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 = {}
:
... 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!
... 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
:
... 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:
... 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
:
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()
:
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:
... 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 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: open parenthesis, function, open parenthesis, close parenthesis,
open curly brace, then indent everything. At the bottom, add the closing curly, closing parenthesis
and then ()
:
(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.
Hey Deniz,
Oh, I see what you mean! Well, first of all, our JS courses are meant that developers already have some strong basic knowledge about JS, i.e. like how it works, the basic syntax, i.e. can already write code in JS. In these tutorials we just mostly show how to improve your code quality. If you know what I mean. So, if you want to learn JS in more deep with basics - I'm afraid we don't have a good course on SymfonyCasts for you yet.
That's a good question... and as always it depends on your own project! First of all, when we're counting with JS on the frontend, and about "counting" I mean doing any heavy operations actually - those operations are done on the *client* side, and so it cause no performance hit for your *server* side. But counting with SQL query would hit both your PHP server and SQL server. We just gave a possible example here, but depends on your business you might want to go a different approach. And another reason why it's done this way - complexity. With calculating on the fly we don't have to send any AJAX requests and then handle possible success / failure cases. So, for learning purposes in this exact spot it works well, but you should definitely look at your own case and see if it fits your needs or there should be a better solution for it.
I hope this clarify things for you a bit and I hope this helps!
Cheers!
Hi,
Thanks for the video, just to tell you there is a problem with subtitles at the end of the video
Hey Mickaël M.!
Thanks for letting us know - that happens occasionally with a service we use - we'll get it fixed up!
Cheers!
// composer.json
{
"require": {
"php": "^7.2.0",
"symfony/symfony": "3.1.*", // v3.1.10
"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.0
"symfony/monolog-bundle": "^2.8", // 2.12.0
"symfony/polyfill-apcu": "^1.0", // v1.2.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
"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.1
"symfony/phpunit-bridge": "^3.0" // v3.1.6
}
}
Hey there! This year I've been trying to relearn everything from scratch with the help of you guys. Since most of the projects I've done had to be done in a very tight time frame and with very little budget, leaving me little to no room to "properly" learn things. After finishing the PHP courses, the namespace bonus one and Composer, I've come to go through javascript- I have very little to no javascript experience and while it is "okay" to follow this course, it gets very complicated very quickly. While I can understand core concepts, I feel like I don't quite hit the mark if I finisih this course.
Also making functions to calculate something on the fly *feels* wrong, is this really how it's done in the "real world"? I would've expected that we eventually use functions to get the new weight lifted from the database, to get accurate numbers- I mean these are not wrong, but we're rewriting a logic that could be solved by a simple databse query or am I wrong?
More rambling than question(s), but hey :D