13 May 2009

Creating a custom dialog with jquery and jquery.blockUI

The goal of this article is to demostrate how to create a dialog with it own self-contained functionality using jQuery and blockUI.

For this we need jQuery (v1.3.x) ((jQuery (v1.3.x) from jquery.com)) and blockUI(v2.15+) ((blockUI(v2.15+) from malsup.com)). I assume you have at least basic understanding of jQuery and JavaScript to make use of this article.

I tend to think of a dialog like an interface with it's own enclosure containing attributes, private functions and public functions. The idea is that we can reuse the functionality easily, just include the javascript-file in the webpage and call a function.

The dialog I'll create here is a dialog from which a user will select the size of a pair of jeans.

I pack my code into a namespace, so I create the namespace first ((Covered in Namespacing in JavaScript)). My dialog will be a subset of UI.dialog.

var UI = {
  dialog : {}
};

Now I can create the basic structure of our interface in a new file:

( function( UI, $ ) {
  // Make sure the namespace exists.
  if ( !UI.dialog ) {
    UI.dialog = {};
  }

  // It this interface already loaded?
  if ( UI.dialog.selectSize ) {
    // If so don't intiate it again
    return;
  }

  // Attributes

  // Private functions

  // Public functions
  UI.dialog.selectSize = function( ) {
  };
} )( UI, jQuery );

This piece of code shows the basic code-structure I use to create an interface.

First I create an enclosure which is an anonymous function ((Covered in Reintroducing a little sanity in working with JavaScript)),

which is called immediately with the UI and jQuery parameter. Everything inside this enclosure is unreachable from the outside, unless I intentionally make it public by binding functions or variables to a global variable like UI.
I these three sections in my enclosure:

  1. Attribute section
  2. Private function section
  3. Public function section

In the same way I would have it in a class in C++ or Java, which is the model I used to come up with this.

Now I'll create the HTML part of this dialog. I'll add the function setupHTML to the private functions section.

	function setupHTML( q, msg, sizes ) {
		// We need a group name to make exclusive selection in the radio buttons we are going to create.
		var group_name = "size_" + parseInt( Math.random( ) * 100, 10 );

		// Write the HTML much like the way, we would write it directly in our .html-file.
		var dd = "<div style='text-align : left; cursor : default;'>";
		dd += "<div style='font-weight: bolder; padding-left:20px;padding-right:20px;'>" + msg + "</div>";

		// Add a radio-button for each size in our sizes, array.
		$.each( sizes, function( i, size ) {
			dd += "<div>";
			dd += "<input type='radio' id='" + group_name + "_" + i + "' style='margin-right: 8px;' name='" + group_name + "' value='" + size + "' />";
			dd += "<label for='" + group_name + "_" + i +  "'>" + size + "</label>";
			dd += "</div>";
		} );
		
		// Add buttons
		dd += "<div style='text-align: right; padding : 5px;'>";
		dd += "<button xvalue='okay' disabled='disabled'>Okay</button>";
		dd += "<button xvalue='cancel'>Cancel</button>";
		dd += "</div>";
		dd += "</div>";
		
		q.html( dd );
	}

Now that we have the function for creating the HTML, we need to create the function to open the dialog.
This function is called UI.dialog.selectSize(...) and takes three parameters:

  1. msg // The message to the user
  2. sizes // An array containing the available sizes
  3. callback // The function is called after the user clicks "okay". The first parameter in the callback function is the selected size
	UI.dialog.selectSize = function( msg, sizes, callback ) {
		// There is no point in showing the dialog without both msg and sizes
		if ( msg && sizes ) {
			// Block the whole page
			$.blockUI( {
				message : $( "<div id='select_size_overlay'></div>" ),


				css : {
					width : "auto",
					height : "auto"
				}
			} );
			
			// The container for the dialog is the div I created above,
			// we need to find it and add the content to it.
			var q = $( "#select_size_overlay" );
			setupHTML( q, msg, sizes );
			
			// TODO: Handle events
		}
	};

In this function I haven't added the events yet, this is because I haven't created the event functions yet.
To limit the potential memory leaks in browsers like IE6, I choose not to make them inline event-functions but to make them private functions.
The potential memory leak is that referencing DOM-element in inline function, creates an extra reference to that element which can confuse the garbage collector. The problem is increased if the function is inline, since it's created every time the parent function is call which could be many time.

I put my event functions in the private functions-section of my interface.

	function button_click_event( e ) {
		// Get the element that was clicked, which is e.target.
		// This should work in all browsers since jQuery have been so kind as to fix broken event-objects.
		var el = $( e.target );
		
		// Get data for this event
		// This is important since it holds reference to data from the UI.dialog.selectSize-function.
		var data = e.data;
		
		// The xvalue-attr tells the event-handler which action we want to perform.
		var xvalue = el.attr( "xvalue" );
		if ( xvalue == "okay" ) {
			// Is something selected?
			var radios = data.radios;
			var selected_val = radios.filter( ":checked" ).val( );
			if ( selected_val ) {
				if ( $.isFunction( data.callback ) ) {
					data.callback( selected_val );
				}
				
				$.unblockUI( );
			} else {
				// Should never happen, since the okay button should be disabled
				alert( "You must select a size." );
			}
		} else if ( xvalue == "cancel" ) {
			$.unblockUI( );
		}
	}
	
	function radio_click_event( e ) {
		// Get the element that was clicked.
		var el = $( e.target );
		
		// Get data for this event
		var data = e.data;

		// Toggle okay-button on/off on selection
		var radios = data.radios;
		var okay_button = data.buttons.filter( "[xvalue=okay]" );
		if ( radios.filter( ":checked" ).size( ) > 0 ) {
			okay_button.removeAttr( "disabled" );
		} else {
			okay_button.attr( "disabled", "disabled" );
		}
	}

Now that these event-functions have been created I can bind them in the UI.dialog.selectSize-function:

	UI.dialog.selectSize = function( msg, sizes, callback ) {
		// There is no point in showing the dialog without both msg and sizes
		if ( msg && sizes ) {
			// Block the whole page
			$.blockUI( {
				message : $( "<div id='select_size_overlay'></div>" ),


				css : {
					width : "auto",
					height : "auto"
				}
			} );
			
			// The container for the dialog is the div I created above,
			// we need to find it and add the content to it.
			var q = $( "#select_size_overlay" );
			setupHTML( q, msg, sizes );
			
			// Inorder for the dialog to work, we need to bind some event to the elements in it.
			var buttons = q.find( "button[xvalue]" );
			var radios = q.find( "input[type=radio]" );
		
			// Handle events
			
			// I'm using jQuery's bind function to bind the events to the elements and to parse data to those events.
			// I can't use buttons.click( button_click_event ),

 because I need to parse some extra data to the events.

			// Handle click event for the buttons
			buttons.bind( "click", {
				callback : callback,
				buttons : buttons,
				radios : radios
			}, button_click_event );
			
			// Handle click event for the radio-buttons
			radios.bind( "click", {
				buttons : buttons,
				radios : radios
			}, radio_click_event );
		}
	};

Now I'm done with writing the dialog interface, here is the entire code including jslint parameters ((I prefer to run all my JS-code through jslint to find hidden errors.)):

/*jslint undef: true, browser: true */

/*global jQuery, UI */

( function( UI, $ ) {
	if ( !UI.dialog ) {
		// This is part of the UI.dialog namespace, if the namespace doesn't exist create the namespace object
		UI.dialog = {};
	}

	// It this interface already loaded?
	if ( UI.dialog.selectSize ) {
		return;
	}

	// Attributes

	// Private functions
	function setupHTML( q, msg, sizes ) {
		// We need a group name to make exclusive selection in the radio buttons we are going to create.
		var group_name = "size_" + parseInt( Math.random( ) * 100, 10 );

		// Write the HTML much like the way, we would write it directly in our .html-file.
		var dd = "<div style='text-align : left; cursor : default;'>";
		dd += "<div style='font-weight: bolder; padding-left:20px;padding-right:20px;'>" + msg + "</div>";

		// Add a radio-button for each size in our sizes, array.
		$.each( sizes, function( i, size ) {
			dd += "<div>";
			dd += "<input type='radio' id='" + group_name + "_" + i + "' style='margin-right: 8px;' name='" + group_name + "' value='" + size + "' />";
			dd += "<label for='" + group_name + "_" + i +  "'>" + size + "</label>";
			dd += "</div>";
		} );
		
		// Add buttons
		dd += "<div style='text-align: right; padding : 5px;'>";
		dd += "<button xvalue='okay' disabled='disabled'>Okay</button>";
		dd += "<button xvalue='cancel'>Cancel</button>";
		dd += "</div>";
		dd += "</div>";
		
		q.html( dd );
	}

	/*
	 * Event for handling click on any button in the overlay
	 */
	function button_click_event( e ) {
		// Get the element that was clicked. jQuery fixes the incompatibility between browsers' event-object (e).
		var el = $( e.target );
		
		// Get data for this event
		var data = e.data;
		
		// The xvalue-attr tells the event-handler which action we want to perform.
		var xvalue = el.attr( "xvalue" );
		if ( xvalue == "okay" ) {
			// Is something selected?
			var radios = data.radios;
			var selected_val = radios.filter( ":checked" ).val( );
			if ( selected_val ) {
				if ( $.isFunction( data.callback ) ) {
					data.callback( selected_val );
				}
				
				$.unblockUI( );
			} else {
				// Should never happen, since the okay button should be disabled
				alert( "You must select a size." );
			}
		} else if ( xvalue == "cancel" ) {
			$.unblockUI( );
		}
	}
	
	function radio_click_event( e ) {
		// Get the element that was clicked. jQuery fixes the incompatibility between browsers' event-object (e).
		var el = $( e.target );
		
		// Get data for this event
		var data = e.data;

		// Toggle okay-button on/off on selection
		var radios = data.radios;
		var okay_button = data.buttons.filter( "[xvalue=okay]" );
		if ( radios.filter( ":checked" ).size( ) > 0 ) {
			okay_button.removeAttr( "disabled" );
		} else {
			okay_button.attr( "disabled", "disabled" );
		}
	}
	
	// Public functions
	UI.dialog.selectSize = function( msg, sizes, callback ) {
		if ( msg && sizes ) {
			$.blockUI( {
				message : $( "<div id='select_size_overlay'></div>" ),


				css : {
					width : "auto",
					height : "auto"
				}
			} );
			
			var q = $( "#select_size_overlay" );
			setupHTML( q, msg, sizes );
			
			var buttons = q.find( "button[xvalue]" );
			var radios = q.find( "input[type=radio]" );
		
			// Handle events

			// Handle click event for the buttons
			buttons.bind( "click", {
				callback : callback,
				buttons : buttons,
				radios : radios
			}, button_click_event );
			
			// Handle click event for the radio-buttons
			radios.bind( "click", {
				buttons : buttons,
				radios : radios
			}, radio_click_event );
		}
	};
} ) ( UI, jQuery );

Using the dialog is as simple as this:

	UI.dialog.selectSize( "Select size of the jeans", [
	  "xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large", "xxxx-large"
	], function( size ) {
		// TODO: Do something usefull with the size
		alert( size );
	} );

Tags: