Posterous theme by Cory Watilo

Filed under: jQuery

Yet Another jQuery Validation Plugin (this time with tinyness)

I've used a number of pure JavaScript libraries and jQuery plugins to handle client-side validation over the years but I never really liked any of them.  As I often do when I can't find exactly what I want, I built a new one.  

There are certainly libraries out there that cover a broader set of features, but my goal here was to make it dirt simple.  So simple in fact that anyone should be able to read the source and in a few minutes figure out how to make it conform to novel requirements.  The entire framework is shown here in the following gist:

As you can see it takes up just over 100 lines and offers out-of-the-box support for a number of standard validation patterns.  However, additional validator methods can be added via the options object.  Below is a usage sample that will make things more clear:

In the above example you can see that adding validation to any input element is as simple as defining a "data-validators" attribute to the element.  The value of this attribute is essentially a JSON string with some syntactic sugar to make it easier on the eyes.  For example, you don't have to surround the whole string with curly braces and if you are defining a validator such as "required" but have no additional options to specify you don't have to do "required:{}" for the JSON to be valid.  If you look at the "expandValidatorString" method in the source you can see that these elements are added dynamically before the JSON is evaluated.

At some point I'll put this up as a repository with more complete documentation but I wanted to get it out there for some feedback before that.  Let me know what you think.

jQuery.ganttView a Lightweight Gantt Chart for jQuery

I just wanted to drop a quick post about a new jQuery plugin that I've been working on for a recent project. As the title of the post implies it renders a simple HTML based Gantt chart using jQuery and the excellent date.js library. The screenshot below should give you an idea of what can be done with the plugin.
Media_httpwwwthegrubb_jfagg

The current version is fairly stable and has been tested in FireFox 3.6, Safari 4, Chrome 5, and IE8 and works well in all those browsers. There are likely a few issues with it in IE7 and I wouldn't even bother checking IE6, unless someone sends me a patch I have no interest in supporting IE6.
The source code and documentation can be found on GitHub: http://github.com/thegrubbsian/jquery.ganttView
Let me know what you think and if there are any specific features you'd like to see added.

jQuery Plugin: tablePager

Here's a quick table pager plugin for jQuery. Unlike a number of the other plugins out there, this one doesn't alter the DOM tree of the table itself it just hides the rows that are not available for the current page. In this way you can still get access to all the elements of the table throughout the lifetime of the page. Plugin Source:
(function($) {
    $.fn.tablePager = function(options) {

        /*
        options = {
        firstBtn: Element,
        prevBtn: Element,
        nextBtn: Element,
        lastBtn: Element,
        indicator: Element,
        sizeSelect: Element,
        pageSize: Number,
        onPageChange: Callback,
        onPageSizeChange: Callback
        };
        */

        var tbl = this;
        var defaults = { pageSize: 10 };
        var opts = $.extend(defaults, options);

        var pageIndex = 0;
        var pageSize = opts.pageSize;
        var rowCount = getRows().length;
        var pageCount = getPageCount();

        function init() {
            bindControlHandlers();
            updatePaging();
            updateIndicator();
        }

        function getRows() {
            return tbl.find("tbody tr");
        }

        function bindControlHandlers() {
            if (opts.firstBtn) { 
                opts.firstBtn.unbind().bind("click", 
                    function() { changePage(0); }); 
            }
            if (opts.prevBtn) { 
                opts.prevBtn.unbind().bind("click", 
                        function() { changePage(pageIndex - 1); }); 
            }
            if (opts.nextBtn) { 
                opts.nextBtn.unbind().bind("click", 
                        function() { changePage(pageIndex + 1); }); 
            }
            if (opts.lastBtn) { 
                opts.lastBtn.unbind().bind("click", 
                        function() { changePage(pageCount - 1); }); 
            }
            if (opts.sizeSelect) { 
                opts.sizeSelect.unbind().bind("change", 
                    function() { changePageSize(parseInt($(this).val())); }); 
            }
        }

        function getPageCount() {
            return (rowCount % pageSize) ? 
                Math.floor(rowCount / pageSize) + 1 : (rowCount / pageSize);
        }

        function changePage(toIndex) {
            if (toIndex >= 0 && toIndex = start && i 

Usage:

var pager = $("#testTable").tablePager({
    firstBtn: $(".first"), // The element that pages to the first page
    prevBtn: $(".prev"), // The element that pages to the previous page
    nextBtn: $(".next"), // The element that pages to the next page
    lastBtn: $(".last"), // The element that pages to the last page
    indicator: $(".curpage"), // The input element that stores the current page (i.e. "1/5")
    sizeSelect: $(".pagesize"), // The select element that indicates the current page size
    pageSize: 5, // The initial page size
    onPageChange: function(newIndex) { alert(newIndex); }, // Page change callback
    onPageSizeChange: function(newSize) { alert(newSize); } // Page size change callback
});

pager.changePage(2); // Causes the page to change from outside the plugin
var pageIndex = pager.getPageIndex(); // Returns the current page index
var pageSize = pager.getPageSize(); // Returns the current page size


And here's some example markup for the pager elements:

First  
Prev  
Next  
Last

  

    5
    10
    20



As you can see it's really simple and flexible.  You can supply any elements you want to the pager.  If you don't supply a first and last button elements for example then those will be ignored, if you don't specify an indicator then that will be ignored too.  The most important thing is that your DOM stays intact, this allows you to continue to manipulate the "hidden" portions of the table (i.e. the stuff not on the current page) without having to access some crazy expando property that other pager plugins use to stash the un-shown portions of the table.

As always, I'd appreciate any feedback.

jQuery vs. the ASP.NET Update Panel Revisited - Event Delegation

Recently I was again struggling with the ASP.NET Update Panel killing my JavaScript interactions because it replaces the DOM elements it surrounds on every refresh.  My previous solution for this was simply to rebind those events after each Update Panel refresh was completed.  This works perfectly well, but it always seemed a little heavy handed to me.

So in digging into this issue a little more, I came upon a few posts that talked about using event delegation to solve this problem (thanks to everyone out there who wrote about delegation, cause you're smarter than I am).  The basic idea is that you attach events at a higher level in the DOM tree and use the DOM's event bubbling mechanism to catch them.  For example, let's say we have the following ASPX snippet:

<div id="container">
    <ul>
        <asp:UpdatePanel>
            <li class="listItem">Item One</li>
            <li class="listItem">Item Two</li>
            <li class="listItem">Item Three</li>
            <li class="listItem">Item Four</li>
        </asp:UpdatePanel>
    </ul>
</div>

Now let's assume that you've done this somewhere in JavaScript:

function bindLiEvents() {
    $("li.listItem").bind("click", function() { alert($(this).html()); });
}

When this JavaScript is executed jQuery loops over all the found elements and individually attaches an event handler to each.  So already there is an inherent performance gain to be had by not doing this looping.  Furthermore there is now the problem with the Update Panel.  If the user takes some action that causes the panel to refresh, then the four <li> elements are destroyed and recreated and unless I call bindLiEvents again after the refresh I loose this functionality.

Enter the event delegation concept.  Why not attach my handler to a DOM element that's outside the Update Panel - the <div> element in this case.  This may seem a bit counter intuitive, but if you're aware of how event bubbling works in the DOM you can see how this might work.

Event bubbling simply means that events at lower levels in the DOM tree are propagated to higher levels.  For example when an <li> is clicked in our example, the click event is also fired on the <ul> and the <div> and beyond.  We can use this behavior to our advantage in this instance because event bubbling is intrinsic to DOM elements - when new elements are added they automatically exhibit this behavior.

So now let's look at how we might attach an event to the <div> element:

function bindLiEvents() {
    $("#container").delegate("click", "li.listItem",
        function() { alert($(this).html()); });
}

Don't worry about where the delegate method comes from - it's not native to jQuery but I'll get to that.  What's happening here is that I'm binding a very generic event handler to the <div> element, and telling that handler to listen for click events on elements that match the "li.listItem" selector.  So now, when an event bubbles up from an <li> element the handler on the <div> fires, tests if the event originated on an <li> with a class of "listItem" and then calls the function callback.

There are advantages to using the approach.  First, event handlers are attached only once, and only at a single point which saves time when doing the initial bind.  Second, any new elements added to the Update Panel will automatically fire the delegated event handler - no rebind is required.

So if you want to try this out for yourself, here is a jQuery plugin for the delegate method:

(function ($) {
    var bubbledEvents = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove',
        'mouseover', 'mouseout', 'keydown', 'keypress', 'keyup'];
    var allowedEvents = { };
    $.each(bubbledEvents, function(idx,event) { allowedEvents[event] = true; });
    $.fn.extend({
        delegate: function (event, selector, func) {
            return $(this).each(function () {
                if (allowedEvents[event])
                    $(this).bind(event, function (e) {
                        var el = $(e.target), result = false;
                        while (!$(el).is("body")) {
                            if ($(el).is(selector)) {
                                result = func.apply($(el)[0], [e]);
                                if (result === false) { e.preventDefault(); }
                                return;
                            }
                            el = $(el).parent();
                        }
                    });
            });
        },
         undelegate: function (event) { return $(this).each(function () {
            $(this).unbind(event); }); }
    });
})(jQuery);