jQuery.ready() or (if you’re a good boy who puts all the code at the end of the document) right away. We can delay some initialization until later, right?
For a while now, jQuery has had event delegation. If you know what event delegation is and how it works, go ahead and skip to the next section. But, for those of you who don’t know, here’s a little introductory course.
Normally you would attach an event listener directly to an element, and let the handler go from there. Generally there is absolutely nothing wrong with this, but if the elements that you wish to attach event listeners to are dynamic (they’re constantly being created and/or deleted), this can be a hassle. Another time this can be “bad” is when there are many, many elements to attach to, in which case it’s just slow. Event delegation was designed for these situations.
The premise behind event delegation is pretty much the opposite of real-world delegation. Rather than delegating things to those below us, we delegate to elements higher in the hierarchy. Sometimes we even delegate all the way up to the CEO (document). Let’s take a look at a tiny code sample and walk through it to explain.
With delegation, we attach the listener to an element higher in the hierarchy (
document in this case). Then we add another argument to the call to
on that specifies a selector that we need to match. Other than that, it’s exactly the same as the normal event listener.
Here’s how it works:
documentlistens for click events. Any click that happens on the page will bubble up to the
document(unless it was stopped by another event handler).
- When the
documenthears a click event it checks to see if the event happened on an element that matches the selector we passed in (‘.elements’ in this case).
- If it matches, it fires the event handler.
It’s that simple. One of the best parts is that the
document is created immediately, so you can attach listeners to it within the head of the document and these will still work. If you want to learn more about event delegation, look here.
Many times the delayed initialization works pretty well when working with jQuery plugins. The best way I can explain this concept is through examples. I’ll show two examples of initializing plugins that demonstrate a few of the possible hitches you may run into and how to work with them.
This first example utilizes the jQuery lightBox plugin, which may not be the best plugin, but it works for my example. This plugin attaches itself to links to images, and then when you click on the link, instead of just following the link, it creates a modal box with the image contained inside it. If you are using this with a large gallery or you are using infinite scrolling to load more images in dynamically, the normal initialization might not be the best bet for you. Try this:
We delegate a click event listener on the
document to limit the amount of code that runs right away. This delegation makes sure we don’t set the plugin up until we need it and only on the elements that need it at the moment. So, when a gallery link is clicked, we initialize the lightbox plugin on that one link. We need to trigger a new click event on it right away so that lightbox will respond to the click. Then we need to prevent the default action so that we don’t follow the link to a different page. The nice thing about the lightbox plugin for this example is that it automatically prevents bubbling, so once the lightbox plugin is initialized on a link, this delegated event handler will never run for that link again. If we weren’t using JSFiddle, you’d see that ‘init’ is only logged the first time that you click an image.
This technique has some pros and cons.
- Really low amount of initial overhead computation.
- We don’t need to wait for DOM ready to set up the event listeners
- Initialize only the elements you need when you need it.
- Works for dynamically added content without any additional work.
- The lightbox must be set up when you click, so there could be a delay between the click and the reaction to the click. This is generally unnoticeable.
- There may be other things that prevent the delegation from reaching the
documentand there is a bit of overhead associated with bubbling all the way up to the
- A wee bit more code to write.
You’ll notice a few distinct differences in implementation between this example and the lightBox example.
- We use “:not(.hasDatePicker)” in the selector. Date Picker assigns this class to an element that the widget has already been initialized on, so we can use that to make sure we don’t initialize the Date Picker on an element that it has already been initialized on. This is nice because the Date Picker doesn’t prevent bubbling like the lightBox did, so we need some other way to know not to initialize the element. What’s also nice is that we can use this inefficient selector because it isn’t scanning the document for this selector, it’s only comparing the element we have to the selector.
- We’re using a toastr library instead of console so you can actually see when it’s initialized and not initialized. This of course, doesn’t really matter in real apps.
- We don’t need to trigger a focus event again. The Date Picker is smart enough to know that it should show because its input is in focus already.
- We don’t need to prevent the default action. This is because nothing happens by default when something is focused.
That first point above is one of the key points that you’ll have to think about each time you attempt to delay initialization like this. You have to find a way to make sure that the initialization doesn’t happen multiple times. In the first example, lightBox’s prevention of bubbling did that for us. With the Date Picker, we had to check for a class that it adds. For other plugins, you may have to wrap the whole event handler in an
if statement that checks for the initialization state somehow. Some plugins do this themselves, so you can call the initializer all you want and it won’t matter, but I wouldn’t count on it unless you read through the code yourself.