Tuesday, September 4, 2012

Tinkering with jQuery UI Widgets (Part 2): Creating a Custom Widget

In part 1 of this series, we explored chaining several jQuery UI widgets together to enhance our element to do interesting things with the built-in behaviors defined in the UI library. Now we want to create our own widget to encapsulate some new behaviors that utilize the existing UI components and add some new ones specific to our widget.

The pattern for creating jQuery UI widgets is pretty straight forward. Here is the template with some explanations of each section:


(function ($)
{
$.widget("namespace.widgetname",
{
options:
{
// name: value pairs that will
// be set when you call widgetname({options...})
// on an element. Set defaults here
},

_create: function()
{
// Called first time widgetname() is
// called on an element.
// Put one time setup code here that
// are not affected by config options.
// Adding a class to the element is a
// common practice to show that this
// widget has been attached to it.
},

_init: function()
{
// Called everytime widgetname() is
// called on an element. Called after _create
// on initial call on an element
// Put reinitialization code here since
// config options may have been changed
},

_setOption: function(option, value)
{
// Called when widgetname('option' (or 'options'), 'name', value)
// is called. Use switch block to handle each option you've defined
// in the options object.

// Chain to super class (changes in 1.9)
$.Widget.prototype._setOption.apply( this, arguments );
},

destroy: function()
{
// Clean up references you have stored so
// garbage collection can happen. Events are the
// biggest issue. However, good practice is to
// return the element back to its original state
// before this widget enhanced it.

// Chain to super class (changes in 1.9)
$.Widget.prototype.destroy.call( this );
}

});
}
)(jQuery);


Extending the example from the previous post, we'll make a ColorBox widget to contain the functionality we built in that post:


(function ($)
{
// Make your own namespace - do not use 'ui'
$.widget("sdt.colorbox",
{
options:
{
// We're going to have one option for now
// to set the color of the box and set the
// default to 'green'
color: 'green'
},

_create: function()
{
// this.options is now set so we can
// use it to customize our box.
// this.element is a jQuery object
// representing the target element(s)
// 'this' is the widget object not
// a DOM node or jQuery object.
// all of our widget state and data
// can be stored in this so we have
// it throughout the lifecycle of this
// widget.

this.element
.addClass('colorbox')
.css(
{
height: '100px',
width: '100px',
border: '2px solid black'
})
.resizable(
{
aspectRatio: true,
maxHeight: 500,
maxWidth: 500,
minHeight: 100,
minWidth: 100
})
.draggable(
{
grid: [50, 50],
stop: function (e, ui)
{
alert('done dragging!');
}

});
},

_init: function()
{
// Each time colorbox() is called on an element,
// _init is called. On the first colorbox() call,
// _create is called, then _init. One time setup
// tasks that are not affected by the options,
// can go into _create. Since each call
// to colorbox() can change the options, we
// need to handle that here:

this.element
.css(
{
backgroundColor: this.options.color
});
},

_setOption: function(option, value)
{
// If colorbox('option', 'color', 'newcolor')
// is called, its that same as just reinitializing
// the widget. Need to call the default behavior first
// if we're not doing anything special and
// then call init

$.Widget.prototype._setOption.apply( this, arguments );

this._init();
},

destroy: function()
{
// Need to reset this element(s) back to
// what they were before we enhanced them.
// this includes destorying the other UI
// widgets we created.

this.element
.removeClass('colorbox')
.css(
{
height: '',
width: '',
backgroundColor: ''
})
.resizable('destroy')
.draggable('destroy');

$.Widget.prototype.destroy.call( this );
}

});

}
)(jQuery);


Now we can use this widget like any other UI widget:


<div class="boxes">
<div></div>
<div></div>
<div></div>
</div>

<div id="onebox">




$(function()
{
// Collection of boxes
// will use default color: 'green'
$('.boxes > div').colorbox();

// One box - will use color: 'red'
$('#onebox').colorbox({color: 'red'});

});


With minimal code, our widget is doing some pretty amazing things. However, there are some missing pieces we have not discussed. First, why do some of the function start with an underscore (_) and others do not? The answer: functions with underscores are internal or "private" to the widget and not callable by the outside world. Functions without underscores are considered "public". Our widget has 3 "private" functions and 1 "public" function. These are part of the abstract widget class that we are extending and overriding. A public function is called by calling the widget name on an element with the function name as the first parameter and all its arguments as the remaining parameters. So let's say we want to create a way for our box to rotate through several colors. We could create a function animate() in our widget class and then call that method via the colorbox() plugin function:



// Define animate()
(function ($)
{
// Make your own namespace - do not use 'ui'
$.widget("sdt.colorbox",
{
... other widget code
animate: function (colors, speed)
{
// trigger necessary effects
// here
}
... more widget code
}
})(JQuery)

// Call it on our widget
$('mybox').colorbox('animate', ['blue', 'green', 'red'], 100);



The widget framework routes the call to our animate function and passes the last two arguments into colors and speed respectively. Why do this instead of just calling animate directly? Because the UI framework does all the necessary work to return a jQuery object so further chaining can be done. We don't have to do anything. If we do want to return a specific value, we could add a return to the function and the UI framework will avoid the default behavior of returning a jQuery object and, instead, return the value we sent back in our return statement.

The second piece of functionality we need is how to trigger our own events that the outside world can listen to? The UI framework provides a special _trigger function that provides some special behavior not present in the normal jQuery.trigger method. The most notable is defining both a callback function and an event that can be bound using the standard jQuery event methods.

You may have noticed in our widget definition we used the callback "stop" to attach to the draggable's stop event:


.draggable(
{
grid: [50, 50],
stop: function (e, ui)
{
alert('done dragging!');
}

});


We could have also bound to this event like this:


.draggable(
{
grid: [50, 50],
}).bind('dragstop', function (e, ui)
{
alert('done dragging!');
});



Either method is perfectly fine and depends on how you want to organize your code. Now, how to we make our own custom event like draggable's stop? Simple, call _trigger() with the name of our custom event. An event object is automatically created or we can pass one to _trigger in the second argument. Additionally, we can pass along spacial information in the third arguement. Draggable does the same thing - you get a ui object in your handler with special information related to the draggable object. So we could add a "moved" event which is triggered from draggable's stop event. Our widget will add some special information to the data that might be useful to outside handlers:



// Define animate()
(function ($)
{
// Make your own namespace - do not use 'ui'
$.widget("sdt.colorbox",
{
var self = this;

... other widget code
.draggable(
{
grid: [50, 50],
stop: function (e, ui)
{
self._trigger('moved', e, {special: 'my data'});
})
});

... more widget code
}
})(JQuery)

// Handle it on our widget:
$('mybox').colorbox(
{
moved: function (e, info)
{
alert('I moved: ' + info.special);
}
});



Now we have our event tied in - notice how it just passes along the event from draggable's stop and then adds a simple data object to pass to the handler function.

This is the basic process of building a new widget within the jQuery UI library. The pattern is simple and easy to extend so you can make powerful, reusable widgets. A full demo of the ColorBox widget can be found here. The demo includes an actual implementation of the animate function and the "moved" event.

Now that we have a basic widget there is a problem that needs to be addressed. Nothing stops someone from chaining draggable or resizable after calling colorbox to change the behavior you just added inside colorbox. In part 3 of this series, we'll investigate this problem by looking some alternative patterns and solutions.