Menu

Thursday, February 13, 2014

Create a jquery extension for handling dom insert events

Very often when I'm building a web application, I need to dynamically create some objects and append them to the dom. Well thats ok, but if there are some additional calculations that you need to do after inserting your objects, then things are getting a bit messy. For example you need to position your objects according to a relative parent object's dimensions or initialize a plugin that requires your object to appended to the dom.  Well there is always a way of getting around issues like that, but I wanted to keep thing simple and to do that I needed some king of "onDomInsert" event.


Solutions

1. Mutation Observers
One way of getting around this was to detect dom changes using "Mutation Observers". This approach is pretty great and there is a great post on how to use them at html5rocks.com, but the problem is that they aren't that widely supported (http://caniuse.com/mutationobserver). 

2. Detecting dom insertions using css animation.
In my opinion this is a pretty great hack, but still a hack, and because it uses css3 animations, you limit your application usage to only modern browsers. If you want you can read more about that approach here: http://davidwalsh.name/detect-node-insertion

3. Creating a dom insertion event using jquery.
After some thinking, I thought that it wouldn't be so bad to create a jquery extension, that will override some of the dom manipulation methods of jquery with a event trigger. So here it is.

Dom Insert jQuery Extension

The first thing i had to do was to modify some methods to trigger a "beforeDomInsert" and "domInsert" events. After some testing I was able to find that some methods are used in others and there was no point in overriding all of them. For the first part I saved the old methods in a temporary object:


    var parent_methods = {
        // inset inside methods
        /*
         * append
         * appendTo
         * html
         */
        append: $.fn.append,
        /*
         * prepend
         * prependTo
         */
        prepend: $.fn.prepend,
        // insert outside methods
        /*
         * after
         * insertAfter
         */
        after: $.fn.after,
        /*
         * before
         * insertBefore
         */
        before: $.fn.before
    };

Then I created two function - one for before dom insert event and one for dom insert event

     /**
     * Triggers an event if item is a jquery object
     * @param {type} item
     * @return {undefined}
     */
    var on_after_insert = function(item) {
        if (item.triggerHandler) {
            if(item.closest('body').length > 0) {
                item.triggerHandler('domInsert');
                item.find('*').each(function() {
                    $(this).triggerHandler('domInsert');
                });
            }
        }
    };
    
    /**
     * Triggers an event before the element has been inserted
     * @param {type} item
     * @returns {undefined}
     */
    var on_before_insert = function(item) {
        if(item.triggerHandler) {
            if(item.closest('body').length === 0) {
                item.triggerHandler('beforeDomInsert');
                item.find('*').each(function() {
                    $(this).triggerHandler('beforeDomInsert');
                });
            }
        }
    };

Note that here I'm using the 'triggerHandler', because 'trigger' will cause the event to bubble up and we don't want that, we just need the event handler to be executed on the specific element and sub elements. To make the event bubble down trough the element's structure, we use item.find('*') and trigger the event on all child elements.

Now all we need to do is override the existing jquery methods with our own implementation.

    /**
     * modifys a dom insertion method
     * @param {type} method
     * @return {unresolved}
     */
    var dom_events_modifyer = function(method) {
        return function() {
            var args = Array.prototype.splice.call(arguments,0),
                result = undefined,
                i = 0;
        
            for(i = 0; i < args.length; i++) {
                on_before_insert(args[i]);
            }
            
            result = parent_methods[method].apply(this, args);
            
            for(i = 0; i < args.length; i++) {
                on_after_insert(args[i]);
            }

            return result;
        };
    };
    
    $.fn.append = dom_events_modifyer('append');
    $.fn.prepend = dom_events_modifyer('prepend');
    $.fn.after = dom_events_modifyer('after');
    $.fn.before = dom_events_modifyer('before');

And we're done. Here is the complete jquery extension code:

/** 
 * Adds a domInsert event to dom insertion methods 
 */

(function($) {
    
    var parent_methods = {
        // inset inside methods
        /*
         * append
         * appendTo
         * html
         */
        append: $.fn.append,
        /*
         * prepend
         * prependTo
         */
        prepend: $.fn.prepend,
        // insert outside methods
        /*
         * after
         * insertAfter
         */
        after: $.fn.after,
        /*
         * before
         * insertBefore
         */
        before: $.fn.before
    };
    
    /**
     * Triggers an event if item is a jquery object
     * @param {type} item
     * @return {undefined}
     */
    var on_after_insert = function(item) {
        if (item.triggerHandler) {
            if(item.closest('body').length > 0) {
                item.triggerHandler('domInsert');
                item.find('*').each(function() {
                    $(this).triggerHandler('domInsert');
                });
            }
        }
    };
    
    /**
     * Triggers an event before the element has been inserted
     * @param {type} item
     * @returns {undefined}
     */
    var on_before_insert = function(item) {
        if(item.triggerHandler) {
            if(item.closest('body').length === 0) {
                item.triggerHandler('beforeDomInsert');
                item.find('*').each(function() {
                    $(this).triggerHandler('beforeDomInsert');
                });
            }
        }
    };
    
    /**
     * modifys a dom insertion method
     * @param {type} method
     * @return {unresolved}
     */
    var dom_events_modifyer = function(method) {
        return function() {
            var args = Array.prototype.splice.call(arguments,0),
                result = undefined,
                i = 0;
        
            for(i = 0; i < args.length; i++) {
                on_before_insert(args[i]);
            }
            
            result = parent_methods[method].apply(this, args);
            
            for(i = 0; i < args.length; i++) {
                on_after_insert(args[i]);
            }

            return result;
        };
    };
    
    $.fn.append = dom_events_modifyer('append');
    $.fn.prepend = dom_events_modifyer('prepend');
    $.fn.after = dom_events_modifyer('after');
    $.fn.before = dom_events_modifyer('before');
    
})($);

No comments:

Post a Comment