Friday, September 27, 2013

How To Properly Destroy jQuery UI Widgets

I've often found myself in a scenario where I need to destroy one or more jQuery UI widgets, using the standard method of doing so, within some context. A div, let's say. But, I have to know ahead of time, exactly, which UI widget plugins are used. This is due to the jQuery UI plugin design, and how methods are invoked.

Let's say I want to destroy a button widget. I would do something like:

$( "button" ).button( "destroy" );

Which is fine, except the only downside is that I need to know about all the button widgets when I want to destroy them. That is, if I were to try and call the destroy method on an element that wasn't a widget, I'd get an error, this is because the element isn't a widget — it has no destroy method.

So why do we care? Why should we even bother calling destroy on widgets anyway when we can just remove the parent element from the DOM, destroying the widget as a side-effect? Most of the time, it's probably fine to do just that, but it's not safe. Nor is it good practice. Consider the different ways you might change your code in various areas of your application. You might introduce subtle differences in how you're freeing DOM resources that don't properly clean up the widget.

For example, calling destroy on a widget doesn't just return the element to it's original state, before the widget constructor was called. It also cleans up events associated with the widget — prime targets for memory leaks. Another reason you want to properly call destroy when you're finished a given widget is that your application may introduce customized widgets. These are additions to the base widgets that ship with the framework, and they often include modified destroy functionality. So while today your application doesn't exhibit any side effects by not calling destroy, tomorrow it might. It's probably easier to be diligent in cleaning up after your widgets.

In fact, I'm reluctant to always clean up after my widgets, because as I mentioned, the fundamental architecture of a widget plugin requires that you know what type of widget you're using ahead of time. I can't just call:

$( "button" ).widget( "destroy" );
$( "div.ui-slider" ).widget( "destroy" );

But I would like to do something like this. I want to responsibly tidy up after my widgets, but, the cleanup effort might take place in a different region of the code from which they were created. This means a lot of maintenance effort in terns of keeping track of which widget lives in which context. Here's what I need:

I want a destroy method that I can apply to a jQuery object. This method is idempotent in that it won't fail if non-widgets are part of that jQuery object.

Consider the following buttons. The first three, I'll create button widgets with. The next three are just buttons.

<div>
    <button class="ui">Button 1</button>
    <button class="ui">Button 2</button>
    <button class="ui">Button 3</button>
</div>
<div>
    <button>Button 4</button>
    <button>Button 5</button>
    <button>Button 6</button>
</div>

Now here is a jQuery plugin I can use to safely destroy all button widgets — even if I pass in the three non-widget buttons. The way it works is actually quite simple. If the element has a data object with a destroy function, we call it. jQuery UI widgets have this destroy function where regular elements do not.

(function( $ ) {

$.fn.destroy = function() {
    return this.each( function( i, element ) {
        $.each( $( element ).data(), function( i, data ) {
            if ( $.isFunction( data.destroy ) ) {
                data.destroy();
            }
        });
    });
}

})( jQuery );

$(function() {

    $( "button" ).data( "test", "arbitrary" );
    $( "button.ui" ).button().click( function() {
        $( "button" ).destroy();
    });

});

No comments :

Post a Comment