Object oriented event handling in Javascript using the jQuery plugin model

|
When coding my online password manager, Passbook, I found it hard to wrangle the Javascript code to fit into the many elements that could have been generated on the screen. It is not a new problem to me as I have worked with refactored combination pages that attempts to bring together what used to be 10 or so pages of functionality into an AJAX enabled super page. The result of which is normally a thousand plus lines of unmaintainable code.

For Passbook I decided to solve this problem once and for all. The solution I believe is in objectifying page elements as a block so that a panel with an edit and delete button can be duplicated quickly without having the Javascript code keep track of which panel on the page was clicked and trying to modify that page element. An object oriented approach would mean the page object could edit or delete it self because it knows what it is and what it represents.

While there are some existing solutions that use custom methods to streamline the object oriented process and work around Javascript's event target scoping of "this". I believe a better method existed that did not require so much prototype modification and was more self contained and flexible. My solution is to use jQuery's plugin model to control on page elements, or widgets.

To see the basic pattern it is easiest to first checkout the functional demo. The demo contains two main elements: a widgets container where an add action exists, and a widget controller that offers the user the ability to submit it or remove it. The demo shows the widget manipulating it self, its parent, as well as using a basic ajax callback within it self.

The sequence in which I would normally code this is to first create my HTML code. In this case it is very simple and consists of the following:


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Object oriented event handling in Javascript using JQuery plugins</title>
</head>
<body>
<!-- HTML -->
<h1>My widgets</h1>
<div class="widgetContainer">
<div><a href="#" id="add">Add widget</a></div>

&nbsp;
</div>
</body>

Then the widget container plugin would be created with the add link hooked up to an event. By creating the widget container as a plugin it means the code is not restricted to any page or element. As long as the element ids or classes within the widget matches it can be turned into a functional GUI element. The basic plugin code with the onload initializer is:


// Widget container
(function($) {
// Widget container plugin
$.fn.widgetContainer = function() {
this.each(function() {
// Vars
var wc = $(this);

// Set events
wc.find('#add').click(function(e) { if (e) e.preventDefault(); add(wc) });
});
}

// Add a widget to the container
function add(wc) {
console.log("add clicked");
}
})(jQuery);

// Main
$(function() {
$('.widgetContainer').widgetContainer();
});

The above code is quite compact and by using the plugin model we can have multiple widget containers on the page without making any changes to the code. The benefits of object oriented event handling becomes clearer when we actually create the widget, which is designed to exist with other widgets in the widget container. The widget code is:


// Widget
(function() {
// Widget plugin
$.fn.widget = function(container) {
this.each(function() {
// Vars
var w = $(this);
w.parent = container;

// Set events
w.find('form').submit(function(e) { if (e) e.preventDefault(); submit(w) });
w.find('.remove').click(function(e) { if (e) e.preventDefault(); remove(w) });
});
}
$.fn.widget.template = '<div class="widget"><form action="" method="post"><input value="" type="text"><input value="Action!" type="submit"><a href="#" class="remove">Remove</a></form></div>';

// Remove widget
function remove(w) {
w.remove();
}

// Submit widget data
function submit(w) {
w.css('background', 'red');
$.post('/', w.find('form').serialize(), function(data) {
w.find(':text').val((new Date()).toString());
w.parent.fadeOut();
setTimeout(function() { w.parent.fadeIn() }, 500);
});
}
})();

The above code does not re-use the jQuery parameter because I have it residing in the original widget container code so that it is loaded at the same time. However if you decide to abstract it out into its own file that can be done easily by simply including the jQuery parameter in the last ();

The widget code follows the same pattern as the widget container however as one can see it does a lot more. The widget HTML is stored in $.fn.widget.template, however it can also be placed on page and retrieved using a jQuery selected on initialization, it all depends on how you want to balance dependencies and ease of editability.

The pattern works around Javascript's event limitations by passing the widget object into a new function attached to widget events. This is a simple way to be able to refer back to the object of interest and not just the event target. I will often include the event target (this) as a parameter for functions like submit when there is extra data that needs to be taken into consideration before actions are to be taken.

The final bit of code that ties the widget to the widget container is to update the widget container's add function. The add function needs to be updated to the following so that the widget is inserted into the widget container and initialized with all event handlers.


// Add a widget to the container
function add(wc) {
var widget = $($.fn.widget.template);
widget
.appendTo(wc)
.fadeIn('slow')
.widget(wc);
}

Putting it all together, the full javascript required to make a widget container that can add multiple widget becomes:


// Widget container
(function($) {
// Widget container plugin
$.fn.widgetContainer = function() {
this.each(function() {
// Vars
var wc = $(this);

// Set events
wc.find('#add').click(function(e) { if (e) e.preventDefault(); add(wc) });
});
}

// Add a widget to the container
function add(wc) {
var widget = $($.fn.widget.template);
widget
.appendTo(wc)
.fadeIn('slow')
.widget(wc);
}

// Widget
(function() {
// Widget plugin
$.fn.widget = function(container) {
this.each(function() {
// Vars
var w = $(this);
w.parent = container;

// Set events
w.find('form').submit(function(e) { if (e) e.preventDefault(); submit(w) });
w.find('.remove').click(function(e) { if (e) e.preventDefault(); remove(w) });
});
}
$.fn.widget.template = '<div class="widget"><form action="" method="POST"><input type="text" value=""/><input type="submit" value="Action!"/><a href="#" class="remove">Remove</a></form></div>';

// Remove widget
function remove(w) {
w.remove();
}

// Submit widget data
function submit(w) {
w.css('background', 'red');
$.post('/', w.find('form').serialize(), function(data) {
w.find(':text').val((new Date()).toString());
w.parent.fadeOut();
setTimeout(function() { w.parent.fadeIn() }, 500);
});
}
})();
})(jQuery);

// Main
$(function() {
$('.widgetContainer').widgetContainer();
});

Once again, please see the demo for the full source code and to see it in action. Hopefully this will make your development of Javascript based GUIs much simpler as it has done for me. I understand that it is not a perfect solution but it has served me well in my work and projects by limiting the scope of what I need to focus on into very manageable objects.

1 comments:

史可 阆 said...

Really nice article:).

I guess it will be better encapsulated if the widget function is like
$.fn.widget = function(container) {
this.each(function() {
// Vars
var w = $(this);
w.parent = container;
container.appendTo(w);

// Set events
w.find('form').submit(function(e) { if (e) e.preventDefault(); submit(w) });
w.find('.remove').click(function(e) { if (e) e.preventDefault(); remove(w) });
});
};

Post a Comment