Tuesday, December 29, 2009

jQuery Autocomplete and ColdFusion

While migrating my applications from Prototype to jQuery, I needed to replace the Ajax.Autocompleter that comes with script.aculo.us, the effects library written for Prototype.

After a quick Google search, I found a pretty good plugin that looked like it would do what I wanted. However, there was a pretty big difference in how the two plugins work. Ajax.Autocompleter must return an unordered list (<ul>), with each suggestion rendered inside its own list item (<li>). Piece of cake.

However, the jQuery Autocompleter expects a string, with each suggestion rendered on its own line. This threw me for a loop at first, since ColdFusion isn't always the best at dealing with whitespace. After a little thought, here's what I came up with.

index.cfm

<link rel="stylesheet" type="text/css" href="jquery.autocomplete.css" />
<script src="jquery-1.3.2.js"></script>
<script src="jquery.autocomplete.js"></script>

<script type="text/javascript">
$().ready(function() {
$('#search').autocomplete('UserService.cfc?method=autocomplete&_cf_nodebug=true', {
multiple: true,
formatItem: function(row) {
var user = JSON.parse(row.toString());
return user.name;
},
formatResult: function(row) {
var user = JSON.parse(row.toString());
return user.email;
}
});

$('#search').result(function(event, data, formatted) {
var user = JSON.parse(data.toString());
alert('You selected user '+user.id);
});

});
</script>

<input type="text" id="search" name="search" size="60" />


UserService.cfc

component {

remote void function autocomplete(required string q) {

// get the users
var users = searchUsers(arguments.q);
var result = [];
var i = "";

for (i=1; i <= arrayLen(users); i++) {

// build the user record
var user = {};

// maintain lowercase keys
user["id"] = users[i].id;
user["name"] = users[i].name & " (" & users[i].email & ")";
user["email"] = users[i].email;

// serialize the user record and append it to the result
arrayAppend(result, serializeJSON(user));

}

// convert the result from an array to a list, with each user on its own line
var html = arrayToList(result, chr(10));

// output the JSON result
writeOutput(html);

}

public array function searchUsers(required string search) {

// normally this would query the db,
// but for this demo I'll just create a static array of users
var users = [
{id="1", name="Adam", email="adam@email.com"},
{id="2", name="Bob", email="bob@email.com"},
{id="3", name="Brady", email="brady@email.com"},
{id="4", name="John", email="john@email.com"},
{id="5", name="Kaitlyn", email="kaitlyn@email.com"},
{id="6", name="Leanne", email="leanne@email.com"},
{id="7", name="Lisa", email="lisa@email.com"},
{id="8", name="Mike", email="mike@email.com"},
{id="9", name="Nate", email="nate@email.com"},
{id="10", name="Ryan", email="ryan@email.com"},
{id="11", name="Sean", email="sean@email.com"},
{id="12", name="Tony", email="tony@email.com"},
{id="13", name="Tyler", email="tyler@email.com"}
];

return users;

}

}


On a side note, JSON.parse() is a built-in function only available to newer browsers, like Firefox 3.5 and IE 8.0. Does anybody know of a good plugin to accommodate older browsers? Something similar to Prototype's String.evalJSON would be preferred.

Friday, December 18, 2009

Using onMissingMethod to Treat Properties More Like... Properties

Since I didn't come from a Java background, I'm relatively new to OOP best practices. One thing that I've grown to loathe in my short time working with objects in Hibernate is having to go through getters and setters to access my properties:


<cfset user = new User() />
<cfset user.setFirstName("Tony") />
<cfset user.setLastName("Nelson") />

<cfoutput>
My name is #user.getFirstName()# #user.getLastName()#
</cfoutput>


I'd much rather prefer true implicit getters and setters a la Groovy, ActionScript, C#, etc...


<cfset user = new User() />
<cfset user.firstName = "Tony" />
<cfset user.lastName = "Nelson" />

<cfoutput>
My name is #user.firstName# #user.lastName#
</cfoutput>


After working with jQuery a little more, I really like the convention used on their event helpers, like change() and change(fn). If you pass in an argument, it binds a new observer. If you don't pass in an argument, it triggers the event.

I was able to apply similar logic to my entities in ColdFusion with the help of onMissingMethod() inside a base class that my entities extend.


component {

public void function set(required string property, required any value) {

if (structKeyExists(this,"set#arguments.property#")) {

if (!structKeyExists(arguments,"value") || isNull(arguments.value) || (isSimpleValue(arguments.value) && arguments.value eq "")) {
evaluate("set#arguments.property#(javacast('NULL',''))");
}
else {
evaluate("set#arguments.property#(arguments.value)");
}

}

}

public any function get(required string property) {

if (structKeyExists(this,"get#arguments.property#")) {
local.value = evaluate("get#arguments.property#()");
}

if (!structKeyExists(local,"value")) {
local.value = "";
}

return local.value;

}

public any function onMissingMethod(required string missingMethodName, required struct missingMethodArguments) {

if (structIsEmpty(arguments.missingMethodArguments)) {
return get(arguments.missingMethodName);
}

set(arguments.missingMethodName,arguments.missingMethodArguments[1]);

return this;

}

}



Now if I pass in an argument, it sets the property. If I don't pass an in argument, it gets the property, leaving me with the following code:


<cfset user = new User() />
<cfset user.firstName("Tony") />
<cfset user.lastName("Nelson") />

<cfoutput>
My name is #user.firstName()# #user.lastName()#
</cfoutput>


And in case you didn't read the code, also included in the base class are generic get() and set() methods that relay calls to the actual property getters and setters in order to maintain proper encapsulation.

Wednesday, December 16, 2009

Migrating to jQuery: My First Plugin

A couple posts ago I wrote about how I couldn't justify switching from Prototype to jQuery. Well I take it back (sort of).

After spending more time with jQuery's event handling, I wanted more out of Prototype and I couldn't justify trying to port what I wanted over from jQuery and expect it to work across all browsers. I'd rather let jQuery and its large, active community help me out with testing. So I've begun the process of converting a good portion of my applications over to jQuery.

While updating the code is more tedious than anything, I found myself missing the convenience of Prototype's direct access to DOM elements. For example, I no longer have the following:


if ( $('checkboxA').checked ) {
$('checkboxB').checked = false;
}


Granted with jQuery there are multiple ways to accomplish the same thing but, while they're not too much longer, they're still longer:


if ( $('#checkboxA').attr('checked') ) {
$('#checkboxB').attr('checked',false);
}

if ( $('#checkboxA').is(':checked') ) {
$('#checkboxB').removeAttr('checked');
}

if ( $('#checkbox:checked').length ) {
$('#checkboxB').attr('checked',false);
}


Since I like to write as little code as possible and everybody else seems to really like jQuery's plugin architecture, I figured I'd try write my own plugin. And since it's my first plugin, please don't make too much fun of it.


jQuery.each(('checked,disabled').split(','), function(i, name) {
jQuery.fn[name] = function() {
return arguments ? this.attr(name, arguments[0]) : this.attr(name);
};
});


It works similar to the built-in jQuery event helpers, such as $(selector).change() and $(selector).click(fn). In my plugin, if you pass an an argument, it sets the attribute's value. If you don't pass in an argument, it returns the attribute's value.

Here's how I can write my sample code now:


if ( $('#checkbox').checked() ) {
$('#checkboxB').checked(false);
}


I'll admit it's not much shorter, but it reads a lot better to me. Plus, I don't have to directly call .attr() anymore, which is basically just a generic getter/setter. And if you didn't notice, I also added the same logic for .disabled() for good measure.

I haven't decided if I want to fully embrace a plugin that accomplishes something so trivial, but for some reason I like it.

Monday, December 14, 2009

Handling HTTP Request Parameters in ColdFusion

One thing that always kind of annoyed me in ColdFusion is the separation of HTTP request parameters, meaning parameters for GET requests are put into the url scope and POST parameters are put into the form scope. While this makes sense technically, I find it kind of annoying. For the most part, I don't care where the parameters come from.

Most ColdFusion frameworks alleviate this problem by combining the scopes for you. For example, Model-Glue and Mach-II create an event object that exposes parameters via event.getValue(key) or event.getArg(key). FW/1 combines the variables into request.context, which is then aliased as rc inside your controllers and views. ColdBox does a little bit of both, by having event.getValue(key) inside your controllers and rc inside your views.

While this makes working with request parameters easier, it still feels a little cumbersome having to go through getters and setters or cryptically-named structs (rc? really?). I'd rather have all the parameters combined into a single scope from the get-go.

One of the many things I like about Ruby on Rails and Grails is the params scope, which contains all GET and POST request parameters. Not surprisingly, CFWheels also has the params scope.

However, what happens if you're not using CFWheels, or even a framework at all? I want the same functionality, but without the overhead of adding a framework. With a little inspiration from Ben Nadel, here's my solution:

Application.cfc

component {

function onRequestStart() {

var params = {};
structAppend(params,url);
structAppend(params,form);
getPageContext().getFusionContext().hiddenScope.params = params;

}

}


Now all url and form variables are combined into a single params struct at the beginning of each request, which is then made globally accessible throughout my application, just like a built-in scope. Short and simple, no framework necessary.

On a final note, while this does work, I don't know if all the ColdFusion "experts" would consider it a best practice to add a new "scope" to the language. But in my opinion, it's isolated and helps solve a problem, so why not?

Thursday, December 10, 2009

Firing Native JavaScript Events using Prototype

I've been using Prototype as my JavaScript library of choice for a couple years now. While I'll admit I would probably choose jQuery if I were to rewrite all of my code from scratch, that's simply not an option at this point. I can't justify rewriting perfectly good code for the sake of rewriting it -- if it ain't broke, don't fix it.

For the most part, Prototype has everything I need: Ajax, CSS selectors, DOM manipulation, event handling, and a slew of convenience methods for working with objects and classes. However, I've always been a little bothered by the inability to fire native JavaScript events, meaning there's no way to fire an element's change event. While Prototype allows you to fire custom events using Element.fire, all event names must be namespaced using a colon, which rules out $(id).fire('change').

So today, after some quick Googling, I wrote my own Prototype-based version of jQuery's change() method in order to fire native events. Here's what I came up with:


(function() {
var methods = {};
var events = 'blur,change,click,dblclick,error,'+
'focus,keydown,keypress,keyup,load,'+
'mousedown,mouseenter,mouseleave,'+
'mousemove,mouseout,mouseover,mouseup,'+
'resize,scroll,select,submit,unload';
events.split(',').each(function(event) {
methods[event] = function(element, fn) {
if (fn == undefined) {
if(document.createEvent) {
var evt = document.createEvent('HTMLEvents');
evt.initEvent(event,true,true);
return !element.dispatchEvent(evt);
}
else {
var evt = document.createEventObject();
return element.fireEvent('on'+event,evt)
}
}
else {
element.observe(event,fn);
}
}
});
Element.addMethods(methods);
})();


With this, you can simply call $(id).change() to fire the element's change event or $(id).change(function(){...}) to add an observer to the element's change event.

I did a quick test and it seems to work in both IE8 and Firefox, but I can't guarantee anything about older browsers, so use at your own risk.

On a final note, I don't know why Prototype seems to be overlooked by so many people in favor of jQuery. I actually prefer the Prototype syntax in many cases, but that's probably because I've been working with it longer. Maybe it's because developers who weren't too familiar with JavaScript stumbled upon jQuery one day and decided they didn't have to look at any other libraries out there, which seems really wrong to me. That would be the same as choosing Mach-II without looking at Model-Glue or ColdBox. Or choosing PHP without looking at ColdFusion or Ruby. You really can't know if you've made the right decision unless you've evaluated all your options.