Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Delegate Selectors FTW!

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

So 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!

Your New Best Friend: Delegate Selectors

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 a js-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.

Leave a comment!

8
Login or Register to join the conversation
Jeffrey C. Avatar
Jeffrey C. Avatar Jeffrey C. | posted 4 years ago

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!

Reply

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!

1 Reply
Default user avatar
Default user avatar Chris | posted 5 years ago | edited

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 :)
Reply

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!

1 Reply
Max S. Avatar

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!

Reply

Yo Max,

Haha, another good question which we covered further: https://knpuniversity.com/s... ;)

Cheers!

Reply
Max S. Avatar

I think I might put my questions somewhere first, watch the whole series and recheck them afterwards ;) Thanks Victor!

Reply

Hey Max,

No worry! Actually, you ask right questions.. and it's cool that we covered it ;)

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony... but since it's a JavaScript tutorial, the concepts are still ? valid!

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice