Friday, September 7, 2012

Tinkering with jQuery UI (Part 3): Overriding Default Behaviors with Proxy Functions

In part 2 of this series, we built a custom jQuery UI widget. At this point, our widget creates a box, attaches Draggable and Resizable to the element, provides a method to animate the colors on the box, and augments the drop event to track how far the box was moved. We are now faced with the problem that if Draggable and/or Resizable are called outside out the widget, the settings we made when building our widget (the 50x50 grid, etc) will be overridden. To find a way to avoid this, we have to understand how the internals of the widget object works. The key part is in $.widget.bridge(). This function is the plugin factory that extends $.fn and ensures only once instance of a given widget is created on an element. It also brokers all the method calls to our object instance. By following along in this code, we can see that if an instance is already attached to an element, the options are set and then _init() is called. Tracking the options call we can see that _setOption() is a common intersection where all calls are made to change options on a widget instance. If we could override this behavior for Draggable and Resizable, we could inject the options we want so they can not be overridden for ColorBox. We can do this with proxy pattern to alter the behavior of the function while maintaining the original functionality in appropriate cases:


var _draggable_setoption_orig = $.ui.draggable.prototype._setOption;
$.ui.draggable.prototype._setOption = function(key, value)
{
var _value = value;

if (this.element.data('colorbox'))
_value = _filter('draggable', key, _value);

_draggable_setoption_orig.call(this, key, _value);
}


Here, I'm leveraging the fact that the UI factory records the widget instance in the element's data container hashed by the widget name. If ColorBox is instantiated, then we want to inspect and possibly change the options being set. The _filter() function compares the set of options we want to override and will ensure that those values are set before calling the original _setOption function. Now, if someone tries to change options for Draggable outside of our widget, we'll catch it and fix it before it can be changed.

The only other issue we need to address are the Draggable events. We've augmented the Draggable stop callback to create a new event that calculates the distance the element moved. Any of the jQuery event listeners can be used to catch "dragstop" events on a Draggable. If we don't want that to happen on our Draggable instance, we will need to attach a listener to Draggable and call stopImmediatePropagation() to prevent any other listeners from receiving the event:



...
_create: function()
{
this.element
.addClass('colorbox')
.css(
{
height: '100px',
width: '100px',
backgroundColor: this.options.color
})
.resizable(_conf.resizable)
.draggable(_conf.draggable)
.on('dragstop', $.proxy(function(e) {alert(this.widgetName + ' drag done'); e.stopImmediatePropagation()}, this));
}

...


Here we attached to the "dragstop" event instead of using the callback approach in the original example. Now we can use stopImmediatePropagation() to prevent other handlers from executing. You can see a full example with source in my sandbox.

I prepared this example and it does works as expected. However, it made me wonder if adding the Draggable and Resizable inside ColorBox was an appropriate design decision. As I pondered that, I decided to see what happens to other jQuery UI widgets when overlapping interactions are attached to the same element. For example, consider the Sortable widget. It adds dragging features to a group of child elements and manages the movement of those children so you can rearrange them. If, for some reason, I also add Draggable to those children, what would happen to the Sortable widget? I created a demonstration to show the outcome. As you can see, the Draggable widget will override anything the Sortable had enabled. Considering this behavior, I decided that I needed to consider some guidelines for how I might build a widget.


  • Use the jQuery UI to "mix" non-overlapping interactions, behaviors, and layout enhancements on a target element. In this example, we added Draggable and Resizable to a element inside ColorBox. However, I believe ColorBox should only provide layout to the element - the decision to add dragging and resizing to the element should left up to the developer using ColorBox. Building small building blocks like this allows for better control and reuse. We could still have the proxies in ColorBox so that if Drabbable and Resizable are added to the element, we can customize their behavior.

  • Using other UI widgets inside another UI widget is fine if they are used on children of the target element. If the widget is creating dynamic content in the target element or enhancing existing markup, it makes sense to use those UI widgets. In this situation, the new UI widget is essentially controlling/coordinating the behavior of multiple UI widgets.
  • Use caution when adding UI widgets to elements. If there is overlapping functionality, odd behaviors may occur that can be difficult to trace. I don't think the test case I did for the Sortable and Draggable example is a problem - its just something to be aware of and code accordingly. Generally, you will not do what I did on purpose, however, as pages become more complex and dynamic, many UI widgets may get instantiated. Ensuring they are all attached to the correct elements is important to avoid hours of debugging hassle.



So that's the basics of building jQuery UI widgets. They provide many possibilities for building powerful UI components on top of jQuery's existing foundations. There are some definite design consideration to be aware of while constructing your widgets. But overall, the framework is a great toolset for building stateful and dynamic extension to jQuery.