Saturday, April 7, 2012

jQuery delegate and on subqueries

I was puzzled at first when I found out that $.fn.delegate and $.fn.on do not use the current collection as the context for the subquery. Instead, they run the query from the document root. In other words, with the following HTML structure:
<div id='blog'>
  <ul>
    <li>
      <h3>I Like Ice Cream</h3>
      <div>
        <h3>My Favorite Flavors</h3>
        <ul>etc.</ul> 
      </div>
    </li>
    <!-- more li's with same structure --> 
  </ul> 
</div> 
I wanted to set up click handling on the h3 tags to replace the brief summary content in the content div's below with the full entries loaded via Ajax, but ensure that any h3 elements in the content div's did not accidentally become clickable. And, since the div content is arbitrary, that would be a possibility.

So, I tried:
$('#blog>ul').delegate('click', '>li>h3', handler);
 But, that fails. The $.fn.find method can use a selector that begins with '>' to indicate that the selector path must begin with the direct children of the starting element. Like,
$('#blog>ul').find('>li>h3').text();
would return 'My First Entry'. That works because find uses the current collection as the context for the selector you pass to it.

But, it turns out that the context for delegate is the document root; i.e., the same context that $(selector) uses.

However, I can get the desired behavior by using:
$('#blog>ul').delegate('click', '#blog>ul>li>h3', handler);
(And, yes, it would probably be better if I used a class instead of just h3 to indicate that something is a clickable element, but that just fixes things for one specific case, and still leaves the general issue unresolved.)

So, I asked myself, "why does the processing of the query start at the root context instead of with the collection?"

I finally did come up with a possible good use - if you wanted to locate the elements to delegate to using a different path from the root.

Imagine a page with multiple sections, only one of which is "active" at any time. I could set up the delegating click handling on the blog section, and similar handling on other sections, when the page loads. I could add a class like 'active' to the major section that I wanted to be currently active, and control which delegating handlers actually ran without having to constantly be delegating and undelegating.

The code below demonstrates this (you can see it running at http://www.steveclaflin.com/blog-stuff/jquery/active-delegating.html).
<body>
  <div id="controller" class="active">
    <h1>Master Controller</h1>
    <ul class="liButtonBar">
      <li class="liButton" data-section="#colors">Colors</li>
      <li class="liButton" data-section="#blog">Blog</li>
    </ul>
  </div>
  <div id="colors" class="section active">
    <h2>Try Out Some Colors</h2>
    <div id="colorSample" class="floatBox"></div>
    <ul>
      <li class="liButton">Red</li>
      <li class="liButton">Green</li>
      <li class="liButton">Blue</li>
    </ul>
  </div>
  <div id='blog' class="section">
    <ul>
      <li>
        <h3>I Like Ice Cream</h3>
        <div>
          <h3>My Favorite Flavors</h3>
          <ul>
            <li>Chocolate Almond</li>
            <li>Turtle</li>
            <li>Big Papa</li>
          </ul>
        </div>
      </li>
      <li>
        <h3>Programming</h3>
        <div>
          <p>Programming is fun! And, sometimes, it isn't!</p>
        </div>
      </li>
    </ul>
  </div>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js">
  </script>
  <script type="text/javascript">
    $(document).ready(function() {

      // manage which section is "active"
      $('#controller').on('click', '.liButton', function() {
        $('.section.active').removeClass('active');
        $($(this).data('section')).addClass('active');
      });

      // set up delegating click handling in which sub-selector
      // looks for presence of .active on container
      $('#colors').on('click', '.active .liButton', colorHandler);
      $('#blog>ul').on('click', '.active>ul>li>h3', blogHandler);

      function colorHandler() {
        $('#colorSample')
          .css('backgroundColor', $.trim(this.innerHTML).toLowerCase());
      }
      function blogHandler() {
        alert(this.innerHTML);
      }
    });
  </script>
</body>