Wednesday, October 14, 2009

 

jQuery, how do I love thee?

So I spent a few days doing some JavaScript, which is not my favourite thing. Or at least it didn't use to be. But now that I have discovered JQuery, I gotta say, it's a whole lot more tolerable than it used to be.

JQuery makes some things that I would previous have considered all but impossible downright easy. Take drag-and-drop inside the browser, f'rinstance. To do it yourself, you'd have to deal with, jeez, I don't even know what, DOM craziness, layers, who knows. In jQuery? Well, lemme give you a quick example, stripped of all the actual business logic, of a drag-and-drop form builder:


<html><head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.js"></script>

<style type="text/css">
.columns:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
* html .columns {height: 1%;}
.columns{ display:inline-block; }
.columns{ display:block; }
.columns .column{
float:left;
display:inline;
min-height:360px;
}
.columns .last{ float:right; }
.columns .first{ width:180px; background-color:#ffeeee; }
.columns .second{ width:280px; margin-left:10px; background-color:#eeffee; }
.columns .last{ width:210px; background-color:#eeeeff}

.footers{ display:inline-block; }
.footers{ display:block; }
.footers .footer{
float:left;
display:inline;
}
.footers .last{ float:right; }
.footers .first{ width:180px; background-color:#eeeeee; }
.footers .second{ width:280px; margin-left:10px; background-color:#eeeeee; }
.footers .last{ width:210px; background-color:#eeeeee}

.formLineEdit { text-align: right; }
.optionLabel { float:right; text-align: right; }
.label { font-weight: bold; vertical-align: text-top; }
.formInput { text-align: right; vertical-align: text-top; }
.editHeader { text-align: right; }
.selectOption { text-align: right; }
#error { text-align: center; color: #ff0000; }
#success { text-align: center; color: #00ff00; }
#listForms { float: left; text-align: left; }
#viewHelp { float: right; text-align: right; }
#inputOptions { text-align: right; }
#detailHeader { text-align: right; }
#formName {background-color: #eeeeee; }
#formVersionNumber {background-color: #eeeeee; }
#formSubmitButton {text-align:right; background-color: #eeeeee; }
</style>

<script>
var totalItems=0;
var currentlyEditing=null;
var lastEdited=null;

$(function() {
//make divs created out of XML clickable
var destClicked=false;
$("#destination").mousedown( function() {
if (!destClicked) { //only do this once, or it might get messy
$("#destination").children().each( function() {
$(this).bind("click", function() {
showEditDetailsFor($(this));
});
});
}
destClicked=true;
});

//set up drag-and-drop stuff
$("#textInput").draggable(
{ connectToSortable:'#destination',
cursor:'move',
helper:'clone',
}
);
$("#longText").draggable(
{ connectToSortable:'#destination',
cursor:'move',
helper:'clone',
}
);
$("#selectMultiple").draggable(
{ connectToSortable:'#destination',
cursor:'move',
helper:'clone',
}
);
$("#selectOne").draggable(
{ connectToSortable:'#destination',
cursor:'move',
helper:'clone',
}
);
$("#destination").sortable(
{
change: function(event, ui) {
ui.placeholder.css({visibility: 'visible', border : '2px solid yellow'});
},
start: function(event, ui) {
ui.placeholder.css({visibility: 'visible', border : '2px solid yellow'});
var tempID=ui.item.attr("id");
if (tempID.indexOf("_")==-1) {
tempID=tempID+"_"+totalItems++;
ui.item.attr({id:tempID});
}
},
stop: function(event, ui) {
//load element details, and ensure they'll show up again when this item is clicked
showEditDetailsFor(ui.item);
ui.item.bind("click", function() {
showEditDetailsFor(ui.item);
});
},
}
);
});

function showEditDetailsFor ( object ) {
// object.effect("highlight", {}, 3000);
currentlyEditing=object.attr("id");
if (currentlyEditing==lastEdited)
return;

$(".formLineEdit").hide();
$("#inputOptions").show();

if (object.attr("id").indexOf("textInput")==0) {
var inputLabel = $.trim(object.find(".label").text());
var inputID = object.find("input[name=inputID]").val();
if (inputLabel!="Text Input" || object.find("input[name=inputID]").attr("name")!="inputID") {
$("#textInputEdit").find("input[name=textInputLabel]").val(inputLabel);
$("#textInputEdit").find("input[name=textInputValue]").val(inputID);
}
$("#textInputEdit").show();
}
else if (object.attr("id").indexOf("longText")==0) {
var inputLabel = $.trim(object.find(".label").text());
var inputID = object.find("textarea:first").val();
if (inputLabel!="Long Text" || inputID!="") {
$("#longTextEdit").find("input[name=longTextLabel]").val(inputLabel);
$("#longTextEdit").find("input[name=longTextValue]").val(inputID);
}
$("#longTextEdit").show();
}
else if (object.attr("id").indexOf("selectMultiple")==0) {
var inputLabel = $.trim(object.find(".label:first").text());
var inputID = object.find("input:last").attr("name");
if (inputLabel!="Select Multiple" || inputID!="selectMulti") {
$("#selectMultiEdit").find("input[name=selectMultiLabel]").val(inputLabel);
$("#selectMultiEdit").find("input[name=selectMultiValue]").val(inputID);
blankOption=$("#selectMultiEdit > .selectOption").remove();
var addedChild=false;
object.children(".formInput").each( function() {
optionClone=blankOption.clone();
var optionLabel=$(this).find(".optionLabel").text();
optionClone.find("input[name=multiOptionLabel]").val(optionLabel);
var optionValue=$(this).find("input:first").attr("value");
optionClone.find("input[name=multiOptionValue]").val(optionValue);
cloneRemoveButton = optionClone.find("input[name=removeSelectMultiOption]");
cloneRemoveButton.bind("mouseup", function() {
$(this).parent().remove();
});
$("#selectMultiEdit").append(optionClone);
addedChild=true;
});
if (!addedChild)
$("#selectMultiEdit").append(blankOption);
} //don't show defaults
$("#selectMultiEdit").show();
}
else if (object.attr("id").indexOf("selectOne")==0) {
var inputLabel = $.trim(object.find(".label:first").text());
var inputID = object.find("input:last").attr("name");
if (inputLabel!="Select One" || inputID!="selectOne") {
$("#selectOneEdit").find("input[name=selectOneLabel]").val(inputLabel);
$("#selectOneEdit").find("input[name=selectOneValue]").val(inputID);
blankOption=$("#selectOneEdit > .selectOption").remove();
var addedChild=false;
object.children(".formInput").each( function() {
optionClone=blankOption.clone();
var optionLabel=$(this).find(".optionLabel").text();
optionClone.find("input[name=oneOptionLabel]").val(optionLabel);
var optionValue=$(this).find("input:first").attr("value");
optionClone.find("input[name=oneOptionValue]").val(optionValue);
cloneRemoveButton = optionClone.find("input[name=removeSelectOneOption]");
cloneRemoveButton.bind("mouseup", function() {
$(this).parent().remove();
});
$("#selectOneEdit").append(optionClone);
addedChild=true;
});
if (!addedChild)
$("#selectOneEdit").append(blankOption);
} //don't show defaults
$("#selectOneEdit").show();
}
lastEdited=currentlyEditing;
}


</script>

</head>
<body>
<center><H3>Form Builder</H3></center>
<div id="success"></div>
<div id="error"></div>

<div id="header">
 
</div>
<P/>
<div class="columns">
<div id="source" class="column first">
<b>Form Elements</b><HR/>
<div id="textInput" class="formLine">
<div class="label">Text Input</div><div class="formInput"><input name="inputID" /></div>
</div>
<HR/>
<div id="longText" class="formLine">
<div class="label">Long Text</div>
<div class="formInput"><textarea name="textarea"></textarea></div>
</div>
<HR/>
<div id="selectMultiple" class="formLine">
<div class="label">Select Multiple</div>
<div class="formInput"> 
<input type="checkbox" name="selectMulti" value="one" />
<div class="optionLabel">One</div>
</div>
<div class="formInput"> 
<BR/><input type="checkbox" name="selectMulti" value="two" />
<div class="optionLabel">Two</div>
</div>
</div>
<HR/>
<div id="selectOne" class="formLine">
<div class="label">Select One</div>
<div class="formInput"> 
<input type="radio" name="selectOne" value="one" />
<div class="optionLabel">One</div>
</div>
<div class="formInput"> 
<BR/><input type="radio" name="selectOne" value="two" />
<div class="optionLabel">Two</div>
</div>
</div>
</div> <!--source-->
<div id="middle" class="column second">
<b>Form</b> (drag elements here)
<HR/>
<div id="destination"> </div>
<HR/>
</div>
<div id="details" class="column last">
<div id="detailHeader"><b>Element Details</b><HR/></div>
<div id="textInputEdit" class="formLineEdit">
<div class="editHeader">
<b>Text Input</b>
<input type="submit" name="textInputSubmit" value="Done" />
<input type="submit" name="delete" value="Delete" />
</div>
<BR/><a href="/formHelp#labels" target="_blank">Label</a><input name="textInputLabel" type="text" />
<BR/><a href="/formHelp#ids" target="_blank">ID</a><input name="textInputValue" />
</div>
<div id="longTextEdit" class="formLineEdit">
<div class="editHeader">
<b>Long Text</b>
<input type="submit" name="longTextSubmit" value="Done" />
<input type="submit" name="delete" value="Delete" />
</div>
<BR/><a href="/formHelp#labels" target="_blank">Label</a><input name="longTextLabel" />
<BR/><a href="/formHelp#ids" target="_blank">ID</a><input name="longTextValue" />
</div>
<div id="selectMultiEdit" class="formLineEdit">
<div class="editHeader">
<b>Select Multiple</b>
<input type="submit" name="selectMultiSubmit" value="Done" />
<input type="submit" name="delete" value="Delete" />
</div>
<BR/><a href="/formHelp#labels" target="_blank">Label</a><input name="selectMultiLabel" />
<BR/><a href="/formHelp#ids" target="_blank">ID</a><input name="selectMultiValue" />
<BR/><input type="submit" name="addSelectMultiOption" value="Add Option" />
<HR/>
<div id="selectMultiOption" class="selectOption">
<i>Option</i>: <a href="/formHelp#editSelects" target="_blank">Name</a> <input name="multiOptionLabel" size=12/>
<BR/><a href="/formHelp#editSelects" target="_blank">Value</a><input name="multiOptionValue" size=12/>
<BR/><input type="submit" name="removeSelectMultiOption" value="Remove" />
<HR/>
</div>
</div>
<div id="selectOneEdit" class="formLineEdit">
<div class="editHeader">
<b>Select One</b>
<input type="submit" name="selectOneSubmit" value="Done" />
<input type="submit" name="delete" value="Delete" />
</div>
<BR/><a href="/formHelp#labels" target="_blank">Label</a><input name="selectOneLabel" />
<BR/><a href="/formHelp#ids" target="_blank">ID</a><input name="selectOneValue" />
<BR/><input type="submit" name="addSelectOneOption" value="Add Option" />
<HR/>
<div id="selectOneOption" class="selectOption">
<i>Option</i>: <a href="/formHelp#editSelects" target="_blank">Name</a><input name="oneOptionLabel" size=12/>
<BR/><a href="/formHelp#editSelects" target="_blank">Value</a><input name="oneOptionValue" size=12/>
<BR/><input type="submit" name="removeSelectOneOption" value="Remove" />
<HR/>
</div>
</div>
</div> <!-- details-->
</div> <!--columns-->
<P/>
<div class="footers">
<div id="formName" class="footer first"><b>Form Name</b>: <input name="formName" value="default" size=10 length=64 /></div>
<div id="formVersionNumber" class="footer second">
<b>Version Number</b>:
<input name="formVersion">
</div>
<div id="formSubmitButton" class="footer last"><input type="submit" name="saveForm" value="Finished - Save Form!" /></div>
</div>

</body>
</html>


Pretty slick, eh?

What you do is, you drag form elements from the first column into the second column, where clones are inserted. (That way the element stays in the first column, so you can have an arbitrary number of instances in the second column.) One key thing to note is that clones do not inherit the clonee's bindings, so you have to bind any events after they're created, as shown above in the "stop" event of the "sortable" definition.

I would try to explain more, but either you already know JQuery reasonably well, in which case the above is probably pretty clear already, or you don't, in which case it will be Greek. So - just copy and paste the above as HTML, launch it in a browser, and play around with it; it should drag and drop right out of the box.

Labels: , , , , , , , , ,


Friday, May 29, 2009

 

Ajax, king of Salamis, son of Telamon and Periboea. No, wait. That other Ajax.

So I've been playing around with the web interface to my middleware lately.

(The basic information stream for the app, at least initially, is "Android uploads GPS data, some brief notes, and maybe a picture to the middleware; middleware emails reminder to user to do something with it; user subsequently logs in to web site, conceivably from Android's browser, and uses previously uploaded data to update WikiTravel." I'm gonna add a more direct route that doesn't involve the web site for simple updates, but 'remind me to write this up in more detail later' seems like a more useful function, thanks largely to phone screen sizes.)

Anyway, I stopped writing code just as Ajax - that's Asynchronous Java/XML, more or less, although it has a highly fungible definition - began to take over the web world and begat Web 2.0. So as a mere user, I've always been pretty impressed by it. Saving and loading data within the browser? Cool! Must be tricky!

It ain't. On the contrary, it's really easy. Oh, I can see how you could tangle yourself up in knots with it, but for basic "go send stuff to server / revamp this piece of this page" stuff, Ajax is totally straightforward. Looks like it's often far simpler than doing everything on the server, even.

I did solve one microproblem, so I'll post some code. Ajax is built around an XMLHttpRequest object, which you basically instruct to "go send this request to the server, then get back to me." You then set a callback function XMLHttpRequest uses to get back to you. But I wanted to pass a parameter to my callback function. I wasn't clear on the best way to do this, so I googled, but while there were a few solutions out there, frankly, I think this one I came up with is more elegant. (And I'm sure I'm far, far from the first to work this out.)

Basically, you use the fact that JavaScript treats functions as objects, and write a function to create your callback function on the fly. Sound complicated? It really isn't. Here's JavaScript code to update/expand div with new data from the server:


var http;

function getResponseFunction(divName) {
return function() {
if (http.readyState==4) { //4 means "done!"
var element = document.getElementById(divName);
element.innerHTML = http.responseText;
element.style.display='block';
}
}
}

function fillDiv(divName) {
var element = document.getElementById(divName);
http = new XMLHttpRequest();
http.onreadystatechange=getResponseFunction(divName); // customized callback function
var targetUrl="/expandDiv?divName="+divName; // a call to this URL will return the HTML with which we'll fill the div
http.open ("GET", targetUrl, true);
http.send(null);
}


Voila. Piece o' cake.

(Yes, this project has now expanded to include Java, Python, and JavaScript/DHTML/Ajax code. Hey, it keeps me from getting bored.)

Labels: , , ,


This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]