How to Make JQueryUI Widgets
I’ve been learning how to make widgets with JQueryUI. We’ll work through a real world example.
The Problem
Let’s start with the problem. I have a web app that has tons of dialogs that need to be validated. So lets look at what the validation code looks like for a typical dialog:
function doLogin() { var username = $( "#loginUser" )[0].value; var password = $( "#loginPassword" )[0].value; $( "#loginError" ).empty().append( "<ul style='width: 90%; margin: 0px auto;'>" ); $( "#loginUser" ).removeClass( "ui-state-error" ); $( "#loginPassword" ).removeClass( "ui-state-error" ); var hasError = false; if ( !username ) { $("#loginError").append( "<li style='text-align:left' >Enter a user name.</li>" ); $("#loginUser").addClass( "ui-state-error" ); hasError = true; } if ( !password ) { $("#loginError").append( "<li style='text-align:left' >Enter a password.</li>" ); $("#loginPassword").addClass( "ui-state-error" ); hasError = true; } $( "#loginError" ).append( "</ul>" ); if ( hasError ) { $( "#loginError" ).show(); return; } $( "#loginError" ).hide(); $.post( "login?username=" + username + "&password=" + password, function( data ) { if ( data.substring( 0, 13 ) == "Authenticated" ) { $( "#loginDialog" ).dialog( "close" ); reloadLoginWidget(); reloadPreferencesContainer(); } else { $( "#loginError" ).html( data ).show(); } } ); }
This function gets called when a user clicks the login button on a simple dialog with a user name and password. The code checks that the user name and password fields are filled in, and writes errors into #loginError
.
And here, for reference is what the html for this dialog looks like:
<table style='width:100%'><tr><td> <tr><td> <label for='loginUser' >User Name: </label> </td><td> <input id='loginUser'/> </td></tr> <tr><td> <label for='loginPassword' >Password: </label> </td><td> <input type='password' id='loginPassword'/> </td></tr> </table> <div id='mimoFlowLoginError' style='margin-top:10px; margin-bottom:5px; padding:5px; ' class='ui-tabs ui-widget ui-widget-content ui-corner-all ui-state-error'> </div> <div id='loginSubmit'> <input type='submit' onClick='doLogin();' value='Login' style='margin-top:10px' /> </div>
It works fine, but it has a bunch of drawbacks.
First for each error I want to report I have to write at least 3 lines, one to set the error, at least one to put the referenced input element into an error state, and another line to track if I have an error. This isn’t too bad for a dialog with just two inputs like this one, but many dialogs will have many more input elements.
At the top of the function I have to clear #loginError
, and reset all the elements out of the error state. In the middle I have to check if I should display #loginError
The other problem is that I have to repeat all the error styling inline as I go. If I ever need to change the way the errors are styled, I’ll have to update hundreds of lines of code.
How a Widget Would Help
Conceptually what I have is an error log. I’d like to dump errors into it, and have it display all the errors, correctly styled. I’d like this log to know if it has any errors in it, and if so to display itself automatically. And I’d like it to keep track of what dialog elements have been set to an error state, so it can set them back to normal when the errors are cleared.
I can do this with a widget. Lets call it miniErrorLog
. Here is what the doLogin()
function looks like using the widget:
function doLogin() { var username = $( "#loginUser" )[0].value; var password = $( "#loginPassword" )[0].value; $("#loginError").miniErrorLog( 'clear' ); if ( !username ) $("#loginError").miniErrorLog( 'addError', "Enter a user name", "#loginUser"); if ( !password ) $("#loginError").miniErrorLog( 'addError', "Enter a password", "#loginPassword"); if ( $("#loginError").miniErrorLog( 'hasError') ) return; $.post( "login?username=" + username + "&password=" + password, function( data ) { if ( data.substring( 0, 13 ) == "Authenticated" ) { $( "#loginDialog" ).dialog( "close" ); } else { $( "#loginError" ).miniErrorLog( 'addError', data ); } } ); }
All the cruft is gone. Adding an error only takes a single line. And it keeps track of what page elements have been put into an error state. Finally all the styling for errors is handled by the widget. So I can change it once, and it will apply everywhere.
How To Make a Widget
JQueryUI has a “widget framework”. It is a coding framework that makes it relatively simple to create new widgets. We’ll skip all the theory behind it, and just get to the practicalities.
To declare a new widget you call the $.widget()
function with two arguments, the name of your widget, and a set of “object literals” that describe the data members and method if your widget, like so:
$.widget( 'ui.miniErrorLog', { ... widget code here... });
All widget names must start with ui.
. The part that follows that is used to initialize a DOM element as an instance of that widget. Like so:
$( "#errorLog" ).miniErrorLog();
Inside the curly braces you define data members and methods using the following syntax:
{ data: "a data element", doStuff: function( arg ) { alert( "do stuff with " + arg ); }, }
Each data member or function is introduced with its name followed by a colon, and then it definition, and then a comma.
You can call methods on a widget in the usual way:
$( "#errorLog" ).miniErrorLog( 'doStuff', "my stuff" );
The arguments to any method just follow the method name as additional arguments to the call to miniErrorLog
A Complete Widget Implementation
So here is the code to our widget:
$.widget('ui.miniErrorLog', { logStart: "<ul style='width: 90%; margin: 0px auto; list-style: none;'>", logEnd: "</ul>", errStart: "<li style='text-align:left'>", errEnd: "</li>", content: "", refs: [], _create: function() { $(this.element).addClass( "ui-state-error" ).hide(); }, clear: function() { this.content = ""; for ( var i in this.refs ) $( this.refs[i] ).removeClass( "ui-state-error" ); this.refs = []; $(this.element).empty().hide(); }, addError: function( msg, ref ) { this.content += this.errStart + msg + this.errEnd; if ( ref ) { if ( ref instanceof Array ) this.refs = this.refs.concat( ref ); else this.refs.push( ref ); for ( var i in this.refs ) $( this.refs[i] ).addClass( "ui-state-error" ); } $(this.element).html( this.logStart + this.content + this.logEnd ).show(); }, hasError: function() { if ( this.refs.length ) return true; return false; }, });
Let’s break it down. We start with a bunch of data members ( logStart, logEnd, errStart, errEnd, content, refs
). We can refer to them inside the widget code as javascript data members like this:
doStuff: function() { alert( this.logStart ); };
You can access the widget’s DOM elements using JQuery selectors and the keyword element
. This for example would hide your widget:
doStuff: function() { $( this.element ).hide(); }
When widgets are constructed, the widget framework will call their _create:
method, so we put initialization code in there. In our case, all we’re doing is styling the widget with ui-error-state
and hiding it.
For this widget we implement only three methods:
addError:
which add an error and styles any referenced DOM element with ui-state-error
. It keeps track of any elements that have been styled this way so we can clear the error style later.
clear:
clears out the contents of the widget, and sets any referenced DOM elements back to their normal non-error state.
hasError
will return true if the widget contains an error.
Resources:
JQueryUI | The JQueryUI home page and documentation |
Understanding jQuery UI widgets: A tutorial | This covers more of the theory behing JQueryUI Widgets |
3 ways to define a JavaScript class | Short Tutorial on Creating Javascript Classes |