erichynds

Hi, I'm Eric Hynds, a front-end website developer living outside of Boston, Massachusetts. 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: , ,

  • http://hotmeteor.tumblr.com hotmeteor

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

  • http://twitter.com/jethrolarson Jethro Larson

    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.

  • http://twitter.com/jethrolarson Jethro Larson

    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.

  • http://eliotshepard.com slowernet

    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?

  • ehynds

    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.

  • ehynds

    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.

  • anonguy

    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.

  • Elijahr

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

  • http://twitter.com/coldfuser Drew Wells

    You’ve won a special place in my heart for using duck punch in your blog post. You’ve gained a new reader

  • http://twitter.com/browntim browntim

    Anyone noticed this causing issue with jquery ui 1.8.4 dialog creation. It messes with the creation of my buttons and title bar stored in the title attribute of the dialog div. When I remove the jquery.create.js file the dialog works just fine…also as per @Elijahr’ comment I had to add

    if (node.length == 0){
    continue;
    }

    to line 57 to get this to work as node[0] was undefined since no elements matched the selector the create live event was being applied to.

    Any insight into this issue would be much appreciated. Awesome work this is exactly what we were looking for minus the aforementioned hiccup with jquery ui dialog.

    Thanks
    Tim

  • Tagny Daggart

    Why isn’t this in jQuery in the first place! Great work, thank you!

  • Paolo Mazzoni

    The plugin is great, but even with browntim patch i still have jqueru ui dialog bug!

  • Anonymous

    hello how we use $_POST for this element ?

  • http://twitter.com/sprynmr Bob Spryn

    Any idea if this is still working with jquery 1.6?

  • http://pulse.yahoo.com/_2NCG44KOSI5L4QXZNLDFH7XODA Rumesh

    Hello Everyone,
    JQuery Event handlers are method that is called whenever any event is
    generated or raised in HTML control. For example if I will click on the
    button then event will raise and all element will hide from
    page………… for more details please check out the following link…
    http://mindstick.com/Articles/af5957f6-7c75-4d69-8803-13f0fee3d01f/?JQuery%20Event%20function

    Thanks !!!!
     

  • Albert Scherman

    I honestly still do not know why this isn’t in jquery in the first place.

    Anyhow, what I’d like to know:
    If I create a minified version with uglifyJS, will this still work? I tried, but it doesn’t work so I just want to know if I a) did something wrong; or b) it wasn’t intended for ‘minification’.

    UPDATE: Ah… Nevermind, I did something wrong. ;)