erichynds

Welcome to my online development portfolio and blog. I'm Eric Hynds, a 23 year old website developer living outside of Boston, Massachusetts, and I'm passionate about developing functional, standard-compliant, and user-friendly websites.

jQuery “Create” Event

The livequery plugin allows us to bind events and general logic to all current and future elements. After the inclusion of live() in 1.3, an expansion of supported live event types in 1.4, and the introduction of delegate() in 1.4.2, the livequery plugin has more or less become deprecated. However, it still holds a special place in my heart because of it’s ability to listen for the creation of new elements.

Listening for new elements is most useful when you want to apply logic, like initializing a plugin, to elements that will be added via AJAX. Without livequery, this is easily accomplished by re-binding logic at the point where elements are injected:

// apply a plugin to all current div.foo's
$("div.foo").somePlugin();
 
// do some ajax
$.get("bar.htm", function( response ){
 
	// bind the plugin to the new element, and insert it into the DOM
	$(response).somePlugin().appendTo("body");
});

If you have multiple points of entry, however, this syntax can quickly become hard to maintain and is not very DRY. Livequery allows you to bind it once and forget about it:

// apply somePlugin to all current and future div.foo's
$("div.foo").livequery(function(){
	$(this).somePlugin();
});
 
// do some ajax
$.get("bar.htm", function( response ){
	$(body).append( response );
 
	// somePlugin will be applied to any additional 
	// div.foo's present in "response" automagically
});

To me the appearance of a new element always seems like an event, and it would be cool if this was something you could bind to. The idea here is that once jQuery injects an element into the DOM, any handlers attached to the element’s selector would fire. Luckily, most of jQuery’s DOM manipulation methods (append, prepend, after, before, etc.) eventually funnel down into the $.fn.domManip method. With this in mind, I duck punched $.fn.domManip, added a new “create” special event, making this type of syntax possible:

// when a new div.foo element enters the DOM, call somePlugin() on it.
$("div.foo").live("create", function(){
	$(this).somePlugin();
});

To see a demo of this, click here. The only manipulation method $.fn.domManip does not cover is $.fn.html, so this method is hijacked as well.

And the code (download on GitHub):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// version 1.3 - 07/03/2010
 
(function($, _domManip, _html){
   var selectors = [], gen = [], guid = 0, old = {};
 
   $.event.special.create = {
      add: function( data ){
         selectors.push( data.selector );
      },
 
      // won't fire in 1.4.2 http://dev.jquery.com/ticket/6202
      remove: function( data ){
         var len = selectors.length;
 
         while( len-- ){
            if( selectors[len] === data.selector ){
               selectors.splice(len, 1);
               break;
            }
         }
      }
   };
 
   // deal with 99% of DOM manip methods
   $.fn.domManip = function( args, table, callback ){
 
      // if no create events are bound, just fire the original domManip method 
      if( !selectors.length || $.isFunction(args[0]) ){
         return _domManip.apply( this, arguments );
      }
 
      return logic.call( this, _domManip, arguments );
   };
 
   // deal with the remaining 1% (html method)
   $.fn.html = function( value ){
 
      // if no create events are bound, html() is being called as a setter,
      // or the value is a function, fire the original and peace out.  only string values use innerHTML;
      // function values use append() which is covered by $.fn.domManip 
      if( !selectors.length || $.isFunction(value) || value === undefined ){
         return _html.apply( this, arguments );
      }
 
      // make value an array
      arguments[0] = [value];
      return logic.call( this, _html, arguments );
   };
 
   function logic( method, args ){
      var node, nodes = args[0], html = $(), numSelectors = selectors.length, matches = [];
 
      // crawl through the html structure passed in looking for matching elements.
      for( var i=0, len=nodes.length; i< len; i++ ){
         node = $(nodes[i]);
 
         (function walk( element ){
            element = element || node[0].parentNode;
            var cur = (element ? element.firstChild : node[0]);
 
            while(cur !== null){
               for( var x=0; x<numSelectors; x++ ){
                  if( $(cur).is(selectors[x]) ){
                     if( !cur.id ){
                        cur.id = "jqcreateevt"+(++guid);
                        gen.push(cur.id); // remember that this ID was generated
                     }
 
                     // remember this match
                     matches.push(cur.id);
                  }
               }
 
               walk( cur );
               cur = cur.nextSibling;
            }
         })();
 
         // the html we started with, but with ids attached to elements
         // bound with create.
         html = html.add( node );
      }
 
      // overwrite the passed in html with the new html
      args[0] = html;
 
      // inject elems into DOM
      var ret = method.apply(this, args);
 
      // for elements with a create event...
      $.each(matches, function(i,id){
         var elem = document.getElementById( id );
 
         if( elem ){
            // cleanup generated IDs
            if( $.inArray(id, gen) !== -1 ){
               elem.removeAttribute("id");
            }
 
            // double check to make sure the event hasn't already fired.
            // can happen with wrap()
            if( !$.data( elem, "jqcreateevt") ){
                $.event.trigger("create", {}, elem);
               $.data(elem, "jqcreateevt", true);
            }
         }
      });
 
      return ret;
   }
 
})(jQuery, jQuery.fn.domManip, jQuery.fn.html);

In a nutshell, I’m creating a new special event called “create”, which simply stores the selector used when binding the event into an array. Next, I hijack both $.fn.domManip and $.fn.html methods to walk through any HTML passed in looking for elements with a bound create event. If any matches are found, each element is given a unique ID (if one does not already exist), stored in the matches array, and the original $.fn.domManip method is called to actually inject the HTML into the DOM. Now that the nodes exist in the DOM and not just in a variable, I loop through the matches array, find each element with getElementById, and the trigger the create event. If the element had to be assigned a unique ID it is removed.

Usage Notes

A few notes on how to use this thing/how it works:

  • The event must be bound with live() or delegate() because of the way these methods hold onto the this.selector property. This code uses this.selector to determine if injected elements match the elements bound to create.
  • If you attempt to do any DOM manipulation with $(this) inside the event handler, you’ll trigger infinite recursion.
  • Elements must enter the DOM through one of jQuery’s DOM manipulation functions, or a method that calls it (append, prepend, after, etc.). It won’t work using vanilla JavaScript.
  • Don’t forget that you can return false to prevent this event from bubbling.
  • If no “create” events have been bound, $.fn.domManip or $.fn.html is called straight up without the extra duck punched logic. However, once all “create” events have been removed (via die()), this plugin won’t have any idea because of this bug in 1.4.2.
  • Cross browser compatible (including IE6)!

Download/Demo

Download on Github and view the demos. Unit tests are here.

Tags: , ,

View Comments to “jQuery “Create” Event”

  1. hotmeteor says:

    Awesome. I too have loved the livequery plugin and miss the functionality with live or delegate. This is the best of both worlds!

  2. Nice. You think we should have an event that works on the actual creation event rather than the render? so if you did
    $(“<div>”);
    The event would fire. That way people's code would occur before the DOM write and then accrue no reflow/repaint if their code would otherwise cause it.

  3. Nice. You think we should have an event that works on the actual creation event rather than the render? so if you did
    $(“<div>”);
    The event would fire. That way people's code would occur before the DOM write and then accrue no reflow/repaint if their code would otherwise cause it.

  4. slowernet says:

    Very nice. Thanks. As live() is stated to cover events “now or in the future”, do you think the callback should be applied to matching elements already created in the DOM?

  5. ehynds says:

    Thanks slowernew. If the element already exists in the DOM, it wouldn't make sense to call the create event on it as the element has already been created… this event fires once elements are actually inserted.

  6. ehynds says:

    Thanks slowernew. If the element already exists in the DOM, it wouldn't make sense to call the create event on it as the element has already been created… this event fires once elements are actually inserted.

  7. anonguy says:

    I promise you:
    THIS is the future of UI building with javascript/HTML.

    You may not be able to see the power within, but it's there.

  8. Elijahr says:

    This is fantastic; I have often wished that there was a “create” event, so that livequery handlers could be namespaced. Thank you!

blog comments powered by Disqus