So what’s the point? Why create a plugin for dragging and dropping form elements? I thought I’d try a different take on the typical web-form usability and created what I think is a pretty decent alternative.
Drag and drop elements have been around forever. It’s a pretty good metaphor in terms of describing to the user how certain elements of a user interface are to function. The web is a different matter however. Drag and drop, while isn’t all that new to the web, it hasn’t been as widely adopted with how web sites are designed and are meant to be interacted with.
Dynamic Content
You’ve no doubt encountered forms or pages with various Add buttons to dynamically append new content to a particular page. This functionality can be found on everything from designated landing-page web sites (like iGoogle), news web sites (like the BBC home page), as well as many others.
You can find this in various forms as well, for example if you encountered a form which offers you the option to add multiple email addresses, some forms would just provided you enough blank fields to allow you to do that. Other, more dynamic web sites, may provide a link or button with which you’d press to add new elements to the page without the need of having those static blank fields in the first place (reduces clutter and increases readability).
Extending the Metaphor
I figured, why not extend the metaphor of the drag and drop functionality to these web forms which can be dynamically altered. Wouldn’t it make sense if you wanted to add a new email address to a form that you could drag it from somewhere else and include it?
This is why the DragForm jQuery plugin was born.
Starting the Plugin
I spent some time looking around at different plugins to see how they were developed. One common theme was to self-initialize the plugin so that you don’t leave the dollar sign operator ($) in conflict with other plugins or JavaScript frameworks. This allows you to continue to use the dollar sign at will without potentially buggering up other code elsewhere.
(function($) {
// plugin code goes here
})(jQuery);
The next step was to figure out how I wanted my plugin to be called. By adding my method into the jQuery.fn object, I could then invoke my plugin by calling upon the jQuery selector and then my plugin name, e.g. $('#someID').dragform().
(function($) {
$.fn.dragform = function() {
return this.each(function() {
// some processing per selected element goes here
});
}
})(jQuery);
You can see by the code above that we’re adding our plugin’s method to the $.fn object and we return this.each(). The jQuery Authoring page recommends that you build your plugins in this matter because it “produces clean and compatible code.”
Adding Some Arguments
I wanted to allow the user to specify a few different things. First, they should specify the element where we will be dropping our draggable element. Next, we should allow the user to be able to customize various aspects of how the plugin functions. Let’s modify our code slightly to accommodate this:
(function($) {
$.fn.dragform = function(target, options) {
var settings = $.extend({
class: 'df'
}, options || {});
return this.each(function() {
// some processing per selected element goes here
});
}
})(jQuery);
Our plugin now accepts two arguments: target and options. We haven’t yet done anything with target, but the options extends a default set of parameters that we wish to specify for our plugin. What this does is allows you to call upon the plugin and choose whether to pass in optional arguments or not. If you do not pass arguments in, then the defaulted ones take over.
In the code above, we have an optional argument called class which is defaulted to df.
The Meat and Potatoes
Now down to the heart of it. I want to be able to invoke some code that applies various events to our selected elements when they are dragged and dropped. I created a private function called DragForm which can be called from the $.fn.dragform loop we created earlier.
(function($) {
var DragForm = function(element, options) {
element = $(element);
var settings = $.extend({
uniqueID: false,
uniqueName: false,
class: 'df',
max: null
}, options);
// provide a counter to know how many elements are in the form
var currentTotal = 0;
// set it to be draggable
element.addClass(options.class).draggable({
opacity: 0.75,
helper: 'clone'
});
// public methods
this.dropped = function(target) {
if (settings.max == null || (currentTotal < settings.max)) {
var currentElement = element.find('.df-content').clone().show().appendTo(target);
currentElement.find('.remove').click(function() {
$(this).closest('.df-content').remove();
currentTotal--;
checkTotal();
return false;
});
if (settings.uniqueID || settings.uniqueName) {
// create unique identifier
var uid = 'df-' + (new Date()).getTime();
// add a 'df' hidden input to the content box so that it can be parsed easier
$('<input />', {
name: 'df',
type: 'hidden',
val: uid
}).prependTo(currentElement);
}
// if uniqueID property is true, modify the id values so they're unique
if (settings.uniqueID) {
// find all 'id' values and prepend the uid to them
currentElement.find('[id]').each(function() {
$(this).attr('id', uid + '-' + $(this).attr('id'));
});
// do the same with the 'for' attribute in labels
currentElement.find('label[for]').each(function() {
$(this).attr('for', uid + '-' + $(this).attr('for'));
});
}
// if uniqueName property is true, modify the name values so they're unique
if (settings.uniqueName) {
// find all 'name' values and prepend the uid to them
currentElement.find('[name][name!=df]').each(function() {
$(this).attr('name', uid + '-' + $(this).attr('name'));
});
}
currentTotal++;
checkTotal();
}
};
// private methods
var checkTotal = function() {
if (settings.max != null) {
if (currentTotal >= settings.max) element.addClass('df-disabled').draggable('disable');
else element.removeClass('df-disabled').draggable('enable');
}
};
};
$.fn.dragform = function(target, options) {
var settings = $.extend({
class: 'df'
}, options || {});
// set the target area to be droppable and to accept the class provided
$(target).droppable({
accept: '.' + settings.class,
activeClass: 'df-active',
drop: function(event, ui) {
// call this object's drop method and pass in the target
ui.draggable.data('dragform').dropped(target);
}
});
return this.each(function() {
var dragform = new DragForm(this, settings);
});
}
})(jQuery);
Ignoring the entire DragForm function here (as it’s outside of the scope of this article) you’ll notice that it’s invoked once in the each method. You may be thinking, “why doesn’t he just put all the DragForm functionality within the each method?”
Well, I really only want to have one instance of DragForm running per element, so in order to do so we want to place the DragForm object created within the element’s data store. This not only allows a single instance of the DragForm code, but allows us to return the same instance if it becomes selected again. Potentially you can use this, bound with some form of accessor, to call your selected element and perform operations on it, e.g. $('#someID').dragform('reset');
Summary
Basically the DragForm plugin was merely created as an exercise in learning how to construct a jQuery plugin as well as using the new jQuery 1.4 methods. If you find it of value, feel free to include it in your own site (though I won’t be providing any support… unless it gets popular).
Tags: forms, intermediate, JavaScript, jQuery, plugin, user interface
