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 SubscribeSo dang. Each time we submit, it adds a new row to the table, but its delete button doesn't work until we refresh. What's going on here?
Well, let's think about it. In RepLogApp
, the constructor function is called when we instantiate it. So, inside $(document).ready()
:
... lines 1 - 54 | |
{% block javascripts %} | |
... lines 56 - 59 | |
<script> | |
$(document).ready(function() { | |
var $wrapper = $('.js-rep-log-table'); | |
var repLogApp = new RepLogApp($wrapper); | |
}); | |
</script> | |
{% endblock %} |
That means it's executed after the entire page has loaded.
Then, at that exact moment, our code finds all elements with a js-delete-rep-log
class in the HTML, and attaches the listener to each DOM Element:
... lines 1 - 2 | |
(function(window, $) { | |
window.RepLogApp = function ($wrapper) { | |
... lines 5 - 7 | |
this.$wrapper.find('.js-delete-rep-log').on( | |
'click', | |
this.handleRepLogDelete.bind(this) | |
); | |
... lines 12 - 19 | |
}; | |
... lines 21 - 97 | |
})(window, jQuery); |
So if we have 10 delete links on the page initially, it attaches this listener to those 10 individual DOM Elements. If we add a new js-delete-rep-log
element later, there will be no listener attached to it. So when we click delete, nothing happens! So, what's the fix?
If you're like me, you've probably fixed this in a really crappy way before. Back then, after dynamically adding something to my page, I would manually try to attach whatever listeners it needed. This is SUPER error prone and annoying!
But there's a much, much, much better way. AND, it comes with a fancy name: a delegate selector. Here's the idea, instead of attaching the listener to DOM elements that might be dynamically added to the page later, attach the listener to an element that will always be on the page. In our case, we know that this.$wrapper
will always be on the page.
Here's how it looks: instead of saying this.$wrapper.find()
, use this.$wrapper.on()
to attach the listener to the wrapper:
... lines 1 - 2 | |
(function(window, $) { | |
window.RepLogApp = function ($wrapper) { | |
... lines 5 - 7 | |
this.$wrapper.on( | |
'click', | |
... line 10 | |
this.handleRepLogDelete.bind(this) | |
); | |
... lines 13 - 22 | |
}; | |
... lines 24 - 100 | |
})(window, jQuery); |
Then, add an extra second argument, which is the selector for the element that you truly want to react to:
... lines 1 - 2 | |
(function(window, $) { | |
window.RepLogApp = function ($wrapper) { | |
... lines 5 - 7 | |
this.$wrapper.on( | |
'click', | |
'.js-delete-rep-log', | |
this.handleRepLogDelete.bind(this) | |
); | |
... lines 13 - 22 | |
}; | |
... lines 24 - 100 | |
})(window, jQuery); |
That's it! This works exactly the same as before. It just says:
Whenever a click event bubbles up to
$wrapper
, please check to see if any elements inside of it with ajs-delete-rep-log
were also clicked. If they were, fire this function! And have a great day!
You know what else! When it calls handleRepLogDelete
, the e.currentTarget
is still the same as before: it will be the js-delete-rep-log
link element. So all our code still works!
Ah, this is sweet! So let's use delegate selectors everywhere. Get rid of the .find()
and add the selector as the second argument:
... lines 1 - 2 | |
(function(window, $) { | |
window.RepLogApp = function ($wrapper) { | |
... lines 5 - 12 | |
this.$wrapper.on( | |
'click', | |
'tbody tr', | |
this.handleRowClick.bind(this) | |
); | |
this.$wrapper.on( | |
'submit', | |
'.js-new-rep-log-form', | |
this.handleNewFormSubmit.bind(this) | |
); | |
}; | |
... lines 24 - 100 | |
})(window, jQuery); |
To make sure this isn't one big elaborate lie, head back and refresh! Add a new rep log to the page... and delete it! It works! And we can also submit the form again without refreshing!
So always use delegate selectors: they just make your life easy. And since we designed our RepLogApp
object around a $wrapper
element, there was no work to get this rocking.
Hey Jeffrey C.!
Ah yes! I thought about hooking up the leaderboard to update dynamically, but it just didn't make it into the scope of the tutorial. In general (and this is true of everything in JavaScript), there are 2 ways to approach that problem:
1) The simple way: render it server-side (like it is now), fetch the new leaderboard JSON from the server (maybe you do this automatically after the form is submitted) then (somehow) update the table.
2) Do the same as the above, but render the leaderboard entirely client side - build it via JS on load. Then, when you need to update it, re-render it. That's a bit like what we do in this course with our rep log list, and it's what frontend frameworks do really well.
So, it's "easy" in a sense... but you need to write some code to render the leaderboard via JS. That's the "chore".
Let me know if that helps!
Cheers!
I was just coding and a question came up:
When I need a mouseenter and mouseleave event for the same executable do I have to write it two times, such as:
this.$wrapper.on(
'mouseenter',
'.exampleClass',
this.getTheFunction.bind(this)
);
this.$wrapper.on(
'mouseleave',
'.exampleClass',
this.getTheFunction.bind(this)
);```
Or is there something similar like this:
$('.exampleClass').on({
mouseenter: function() {
....;
},
mouseleave: function() {
....;
}
});
Have a nice day :)
Hey Chris
I had the same problem once and IIRC you can do this:
this.$wrapper.on(
'mouseleave mouseenter', // two events separated by a space
'.exampleClass',
this.getTheFunction.bind(this)
);
Give it a try and let me know if that works ;)
Cheers!
Hey! Is it somehow possible at this point to refresh the form at a successful ajax call as well? Otherwise the form error won't be removed... Thx!
Yo Max,
Haha, another good question which we covered further: https://knpuniversity.com/s... ;)
Cheers!
I think I might put my questions somewhere first, watch the whole series and recheck them afterwards ;) Thanks Victor!
Hey Max,
No worry! Actually, you ask right questions.. and it's cool that we covered it ;)
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
}
}
i have been wondering this whole time the leaderboard doesn't update when i add the weight. Do you need to make a whole function for it?
Have a nice day!