Search code examples
javascriptjqueryprototype-programmingdom-manipulation

Using prototyping to alter functionality of existing jQuery functions


I would like to alter the functionality of $.prepend() (and probably $.append()) for the purpose of having an "on DOM change event".

Can I do something as simple as:

$.prepend = function() { alert('Hello World'); };

Or do I need to use the $.extend() function or $.prototype.prepend or $.fn.prepend?

[I realise I'll need to include the original source for the prepend() function in my new one otherwise jQuery will break!]


EDIT :: Final Solution

For those who are interested:

$.extend($, {
domChangeStack: [],
onDomChange: function(selector, fn, unbind, removeFromStack) {
    /* Needs to store: selector, function, unbind flag, removeFromStack flag */
    jQuery.domChangeStack.push([selector, fn, unbind, removeFromStack]);
},
domChangeEvent: function() {
    /* Ideally should only affect inserted HTML/altered DOM, but this doesn't */
    var stackItem, newStack = [];

    while (stackItem = jQuery.domChangeStack.pop()) {
        var selector = stackItem[0],
            fn = stackItem[1],
            unbind = stackItem[2],
            remove = stackItem[3];

        if (unbind) { $(selector).unbind(); }
        // Need to pass the jQuery object as fn is anonymous
        fn($(selector));
        if (!remove) { newStack.push(stackItem); }
    }

    jQuery.domChangeStack = newStack;

    // Show something happened!
    console.log("domChangeEvent: stack size = " + newStack.length);
}
});

$.fn.prepend = function() {

    var result = this.domManip(arguments, true, function( elem ) {
        if ( this.nodeType === 1 ) {
            this.insertBefore( elem, this.firstChild );
        }
    });

    // Need to actually alter DOM above before calling the DOMChange event
    $.domChangeEvent();

    return result;
};

And usage:

/* Run the given function on the elements found by the selector,
 *  don't run unbind() beforehand and don't pop this DOMChange
 *  event off the stack.
 */
$.onDomChange(".element_class", function(jObj) {
       jObj.do_something_awesome();
   }, false, false);

Solution

  • Which method you want to use depends on how much you need to change. Since .prepend is merely a method that resides in .fn you don't have to mess with the prototype.

    I most cases its enough to rename the original method, create your own function that does what you want and end with a call to the original function, like this:

    var orgPrepend = $.fn.prepend;
    $.fn.prepend = function(){
        // Do what you want here
    
        // Call org prepend and return
        return orgPrepend.apply(this, arguments);
    }
    

    Note: .apply and .call are more or less identical. The only difference is that .apply passes arguments by reference while .call passes them by value, so I prefer to use .apply before .call where possible. See MDC for reference

    But if you look at the source of jQuery (see src/manipulation.js) you'll see that this method is very small so you can just implement it directly.

    In the next example I will use .extend instead, but it's not a must; you could just replace it like in the first example.

    $.extend($.fn, {
        prepend: function() {
            // Do what you want here
            console.log("prepend was called");
            // Execute domManip
            return this.domManip(arguments, true, function( elem ) {
                if ( this.nodeType === 1 ) {
                    this.insertBefore( elem, this.firstChild );
                }
            });
        }
    });
    

    You can override .domManip or any other method in the same way, like in the following example. You'll probably see why I prefer to use .extend here.

    var _domManip = $.fn.domManip;
    $.extend($.fn, {
        domManip: function() {
            // Do what you want here
            console.log("domManip was called");
            _domManip.apply(this, arguments);
        },
        prepend: function() {
            // Do what you want here
            console.log("prepend was called");
            // Execute domManip
            return this.domManip(arguments, true, function( elem ) {
                if ( this.nodeType === 1 ) {
                    this.insertBefore( elem, this.firstChild );
                }
            });
        }
    });
    

    See test case on jsFiddle