0

I have two jquery mobile pages, each in a separate HTML document. When I link to the second document, the pagecreate event fires every time. This has the annoying side effect of initialising the handlers multiple times - once for every time the page has been created.

However, I have noticed that there is a difference between -

$('#btn').on("click", function () {...});

and

$(document).on("click", "#btn", function () {...});

In the first case, the event handler is called only once (which is what I want), but in the second case, it is called once for every time the handler has been initialised. Can someone explain why this is please, and is it safe to rely on the first case to ensure that the handler is only called once?

Here is the initial page:

<body>
    <div id="firstpage" data-role="page">
        <a href="testpageinit_2.php" id="goto" class="ui-btn">Page 2</a>
    </div>
</body>

and the second page:

<body>
    <div id="secondpage" data-role="page">
        <a href="#" id="btn" class="ui-btn">Click Me</a>
        <a href="firstpage" id="btnback" class="ui-btn" data-rel="back">Back</a>
    </div>
</body>

My js looks like this:

$(document).on("pagecreate", "#firstpage", function () {
    alert("First page created");
});

$(document).on("pagecreate", "#secondpage", function () {
    alert("Second page created");

    //  This results in just one handler
    $('#btn').on("click", function () {
        alert("btn clicked handler");
});

    //  This results in a handler being attached every time the page is created
    $(document).on("click", "#btn", function () {
        alert("document clicked handler");
    });
});
engeeaitch
  • 81
  • 3
  • 11

3 Answers3

0

Take a look at jQuery's documentation for the .on() method, found here. There you can read all about delegated event handlers and what they are for.

To summarize the documentation, delegated event handlers bind to an ancestor element of the element that you actually want to listen to for the event. The delegated handler then relies on propagation (or bubbling) to fire the handler when the event reaches the ancestor to which it was bound, in your case it waits for the event to reach $(document).

What this means is that delegated handlers can be used to listen for events triggered by descendant elements that may not be loaded into the DOM yet.

In your case, I would imagine that the elements that you are trying to bind to have not yet been loaded into the DOM when the handler is bound, and so the handler is never bound to them. However, the delegated handler will bind to any current or future elements that match your selector.


Example

Delegated event handlers can be challenging to wrap ones mind around on the first pass. As such, let's take a look at at a use-case to show the difference between delegated and non-delegated event handlers. Have a look at the markup below:

<body>
    <div class="container">
        <ul class="my-list">
            <li>Some item 1</li>
            <li>Some item 2</li>
        </ul>
    </div>
</body>

Imagine that I want to change the background of any .my-list > li element when it is clicked. To do so, I use the following code:

$(".my-list > li").on("click", function() {
    $(this).toggleClass("alternate-background");
});

So far, this will all work exactly as you would expect. However, let's now assume that I have some AJAX that I use to periodically ping the server and request additional .my-list > li elements. Our new markup looks something like this after the first request returns:

<body>
    <div class="container">
        <ul class="my-list">
            <li>Some item 1</li> <!-- Loaded on page load -->
            <li>Some item 2</li> <!-- Loaded on page load -->
            <li>Some item 3</li> <!-- Loaded from AJAX -->
            <li>Some item 4</li> <!-- Loaded from AJAX -->
        </ul>
    </div>
</body>

If this is our use-case and we continue to use the same click-handler as we did before, then we are going to notice a problem: only the backgrounds of the .my-list > li elements that were loaded on page load will change when the element is clicked; the elements added via AJAX will not have the handler bound to them.

However, if we change our click-handler to the following, then the backgrounds of each .my-list > li element (be they added on page load or via AJAX) will change when said element clicked:

$(".container").on("click", ".my-list > li" function() {
    $(this).toggleClass("alternate-background");
});

Notice that in the above, we are still using $(this) to reference the clicked .my-list > li element. Fortunately, jQuery is smart enough to know that we want to use the element that we were listening for, rather than the one that we were listening on, as the context element.

Zachary Kniebel
  • 4,686
  • 3
  • 29
  • 53
  • Many thanks for a very comprehensive answer. I understand that in the first case, I have a direct handler, and in the second case I have a delegated handler, but I don't understand why the direct handler is only called once, even though it is initialised multiple times (unlike the delegated handler)? And can I rely on this behaviour? – engeeaitch Apr 29 '15 at 21:15
  • When you say "it is initialized multiple times" what do you mean? From what I am seeing in your code, your direct handler is being bound only once. Tour delegated handler is also bound only once, yet it is bound to an indeterminable number of elements, because it can also be bound to elements in the future. Thus, on the next `pagecreate`, the new direct handler will be bound to the new `#btn` element while the old direct handler remains bound only to the old `#btn` element. However, both the original and new delegated handlers will be bound to both the old and the new `#btn` elements. – Zachary Kniebel Apr 29 '15 at 21:29
  • Because the pagecreate event for the second page fires everytime the second page is loaded, the event handlers are bound multiple times as you navigate between the first and second pages. However, I have noticed that even though the direct handler has been bound multiple times, when the event occurs the handler is only called once. By contrast, the delegated handler is called multiple times. – engeeaitch Apr 29 '15 at 21:42
  • Do you have a sample page that we can look at? It's just hard to see that functionality in action from the pasted code. – Zachary Kniebel Apr 30 '15 at 15:10
  • I tried to do this in a fiddle, but it needs a second page to be loaded, and I couldn't work out how to do that. Anyway, thanks to your help, I think I have worked out what is going on: – engeeaitch Apr 30 '15 at 17:11
  • 1
    When the user navigates back to the first page, the second page is unloaded, together with any handlers that are bound to elements on that page (the directly bound element in my case). But the handler that is bound to the document, remains. When the user navigates back to the second page, the handlers are again bound, which means that there are two handlers attached to the document, but only one handler attached to the #btn. To add weight to this theory, I changed the delegated handler to bind to the div on the second page, and it then fired only once. – engeeaitch Apr 30 '15 at 17:19
  • @engeeaitch - you should certainly post that as an answer to your question and mark it accepted, when able. While my answer does describe the differences between delegated and non-delegated handlers, I completely overlooked the fact that you the delegated handler was being bound to `$(document)` and that because jQuery mobile's navigation was in use the `$(document)` does not change. – Zachary Kniebel Apr 30 '15 at 17:23
0

There are a couple better solutions to the multiple binding problem:

  1. Do databinding on the 'pageinit' event instead of 'pagecreate'. 'pageinit' is only fired once.

  2. Always remove any existing data binding before adding new binding.

    $(document).off('click', '#btn').on('click', '#btn', function() { ... }

The first method is working for you because the first time it attempts to bind no element yet exists on the DOM with an ID of 'btn', so the binding fails. In the second method you are using what jquery calls "delegated event binding", so the binding works even if the "btn" element is added to the document after the binding. You can read more about it here. For more information about how all this works you should check out this much longer, much more detailed answer.

Community
  • 1
  • 1
Seth
  • 706
  • 4
  • 20
  • I have read that pageinit has been deprecated in favour of pagecreate. Also, I think that the #btn element must exist at the time of binding - because the handler is successfully called after the page is created the first time, as well as when it is created subsequently. – engeeaitch Apr 29 '15 at 21:46
0

Thanks to the help from @Zachary, I think I understand now: When the user navigates back to the first page, the second page is unloaded, together with any handlers that are bound to elements on that page (the directly bound element in my case). But the handler that is bound to the document, remains.

When the user navigates back to the second page, the handlers are again bound, which means that there are two handlers attached to the document, but only one handler attached to the #btn. To add weight to this theory, I changed the delegated handler to bind to the div on the second page, and it then fired only once.

engeeaitch
  • 81
  • 3
  • 11