jqueryjquery-uijquery-ui-widget-factory

Live Binding Events in jQuery UI Widget Factory using this._on()


I am currently developing a cascading tree menu. When a tree item is expanded it creates new <div>'s below it that may also be expanded. I can get the first set of <div>'s to bind and thus expand, but I can't seem to get the newly created div's to bind automatically using the "this._on" method. I'm looking for functionality similar to live(), delegate() and on().

I have posted my code in jsFiddle at http://jsfiddle.net/vEAhq/ .

I set the event binding under the _setupEvents method on line 60.

In review, the elements that are created in the refresh method, called during the _create method bind okay. It's the elements created in the _expand method that do not get bound like I would expect.

Thanks in advance for your time.

$.widget( "custom.categoryMenu", {

    options: {
        links: null
    },

    _create: function() {
        this.refresh();
        this._on('.categoryNavigationExpandIcon', {
            click: function(event) {
                var $container = $(event.target).parent();
                if( ($container).data('expanded') === false ) {
                    this._expand($container);
                } else {
                    this._contract($container);
                }
            }
        });
    },

    refresh: function() {
        var $elm = this.element;
        var $newElem;
        var $itemTree = this.options.links;
        var $itemTreeRoot = sortObj( $itemTree['1'], 'function', true );

        this._setupEvents();

        $.each( $itemTreeRoot, function( key, value ) {
            var ID = $itemTreeRoot[key];
            var label = key;
            $newElem = $( '<div style="padding:8px 0;"></div>' )
                .data( 'expanded', false )
                .data( 'id', ID )
                .data( 'level', 1 )
                .addClass( 'categoryNavigationLevel1' );
            $( '<div class="categoryNavigationExpandIcon"></div>' ).appendTo($newElem);
            $( '<a href="index.cfm?action=products&cat=' + ID + '" rel="?cat=' + ID + '" class="categoryMenuLabel">' + label + '</a>' ).appendTo($newElem);
            // Add triangle icons if there are children
            if ($itemTree[ID]) {
                $($newElem)
                    .find('.categoryNavigationExpandIcon')
                    .addClass('ui-icon')
                    .addClass('ui-icon-triangle-1-e');
            }
            $elm.append($newElem);
        });
    },

    _setupEvents: function() {
        var events = {
            click: function( event ) {
                event.preventDefault();
            }
        };

        this._on('.categoryNavigationExpandIcon', {
            click: function(event) {
                var $container = $(event.target).parent();
                if( ($container).data('expanded') === false ) {
                    this._expand($container);
                } else {
                    this._contract($container);
                }
            }
        });
    },

    _expand: function(container) {
        var $elm = this.element;
        var $itemTree = this.options.links;
        var containerLevel = $(container).data('level');
        var containerID = $(container).data('id');
        var newContainerLevel = containerLevel + 1;

        var $itemTreeBranch = sortObj( $itemTree[containerID], 'function', true );

        $(container)
            .data('expanded', true)
            .find('.ui-icon-triangle-1-e')
            .removeClass('ui-icon-triangle-1-e')
            .addClass('ui-icon-triangle-1-se');

        $.each( $itemTreeBranch, function( key, value ) {
            var ID = $itemTreeBranch[key];
            label = key;
            $newElem = $( '<div style="padding:8px 0;"></div>' )
                .data('expanded', false)
                .data( 'id', ID )
                .data( 'level', newContainerLevel )
                .addClass( 'categoryNavigationLevel' + newContainerLevel );
            $( '<div class="categoryNavigationExpandIcon"></div>' ).appendTo($newElem);
            $( '<a href="index.cfm?action=products&cat=' + ID + '" rel="?cat=' + ID + '" class="categoryMenuLabel">' + label + '</a>' ).appendTo($newElem);
            // Add triangle icons if there are children
            if ($itemTree[ID]) {
                $($newElem)
                    .find('.categoryNavigationExpandIcon')
                    .addClass('ui-icon')
                    .addClass('ui-icon-triangle-1-e');
            }
            $(container).after($newElem);
        });
    },

    _contract: function(container) {
        var containerLevel = $(container).data('level');
        $(container)
            .data( 'expanded', false )
            .find('.ui-icon-triangle-1-se')
            .removeClass('ui-icon-triangle-1-se')
            .addClass('ui-icon-triangle-1-e');

        $(container)
            .nextUntil( '.categoryNavigationLevel' + containerLevel )
            .remove();
    },

    destroy: function() {
        $.widget.prototype.destroy.call(this);
    }

});

/**
* Name: Sort Javascript Object
* Credit: Arne Martin Aurlien
* URL: http://am.aurlien.net/post/1221493460/sorting-javascript-objects
*/
sortObj = function(obj, type, caseSensitive) {
    var temp_array = [];
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (!caseSensitive) {
                key = (key['toLowerCase'] ? key.toLowerCase() : key);
            }
            temp_array.push(key);
        }
    }
    if (typeof type === 'function') {
        temp_array.sort(type);
    } else if (type === 'value') {
        temp_array.sort(function(a,b) {
            var x = obj[a];
            var y = obj[b];
            if (!caseSensitive) {
                x = (x['toLowerCase'] ? x.toLowerCase() : x);
                y = (y['toLowerCase'] ? y.toLowerCase() : y);
            }
            return ((x < y) ? -1 : ((x > y) ? 1 : 0));
        });
    } else {
        temp_array.sort();
    }

    var temp_obj = {};
    for (var i=0; i<temp_array.length; i++) {
        temp_obj[temp_array[i]] = obj[temp_array[i]];
    }

    return temp_obj;
};

Solution

  • It's better, when you have to append some new created content, to bind the events on the document, for a given selector, or for custom events.

    For instance, if you are trying to bind a click on a given class '.your-class' :

    $('.your-class').on('click', function (e) {
        // ...
    })
    

    You would prefer to replace it by that :

    $(document).on('click', '.your-class', function (e) {
        // ...
    })
    

    It is exactly the same for jQuery UI:

    You just would prefer to replace:

    // Here you just bind a click on the existing elements of your class
    this._on('.categoryNavigationExpandIcon', {
        click: function(event) {
            var $container = $(event.target).parent();
            if( ($container).data('expanded') === false ) {
                this._expand($container);
            } else {
                this._contract($container);
            }
        }
    });
    

    By:

    // Bind a custom event on the document in order to delegate this event event to the new created classes
    this._on(this.document, {
        'click.categoryNavigationExpandIcon': function(event) {
            var $container = $(event.target).parent();
            if( ($container).data('expanded') === false ) {
                this._expand($container);
            } else {
                this._contract($container);
            }
        }
    });
    

    You can find the updated the JSFiddle here: http://jsfiddle.net/vEAhq/7/

    By the way, you can skip the closing tag when you create a element with jQuery !

    $('<div>'); // instead of $('<div></div>');