Monday, December 08, 2008

use jQuery AJAX to create options in a dropdown menu

If you love to hate Javascript, that half-breed, amateur-magnet language, and every onload() and eval() you ever saw, then jQuery just rained on your parade. Now, with jQuery, so many things are elegant and easy - the opposite of everything you've ever known about Javascript.

Here's how to populate a select menu's option list (values and labels) with data retrieved from an AJAX request. The impatient may jump to a working demo to see if this is even what you want. Maybe you were searching for some sink cleaning product.

Materials:
  • 1 webpage that loads jQuery and makes an AJAX request (page.html)
  • 1 script that receives the AJAX request and answers it (script.php)
Unlike so many pages written in plain Javascript, which don't function or are missing content in browsers that do not execute Javascript (Lynx, Googlebot, people who dont want your crappy code heating up their CPU and turned it off in Preferences), jQuery's philosophy is that if your browser will execute Javascript, then the page will be better, but if not, then the page should "degrade gracefully" and still at least mostly work. So in this tutorial, the demo page is served with an initial select menu that should be good enough, in case the user doesn't run the script.

Here's the markup that we start with (page.html): A form with an input field, select menu and a button that will trigger our script. In this example, a user enters a zip code in the input box and clicks the button. That will trigger an AJAX request to another resource, sending the zip code and retrieving a bunch of shipping options, which will then magically fill the dropdown menu.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<title>jQuery ajax demo populating select dropdown menu</title>
</head>
<body>
<form>
Zip: <input name="zip" type="text" size="5" maxlength="10" id="zip" value="" /><br />
<select name='shipping_method' id="shipping_method">
<option value="0" selected="selected">Select Shipping Method</option>
<option value='FEDEX_2_DAY' >FedEx 2 Day</option>
<option value='FEDEX_EXPRESS_SAVER' >FedEx 3 Day</option>
<option value='INTERNATIONAL_PRIORITY' >FedEx International</option>
<option value='PRIORITY_OVERNIGHT' >FedEx Priority Overnight</option>
<option value='STANDARD_OVERNIGHT' >FedEx Standard Overnight</option>
</select>
<input id="getrates" type="button" value="Lookup Shipping Rates" /><br />
</form>
</body>
</html>


Notice that there are no Javascript functions strewn into the markup as tag attributes (no onclick(), no onmouseover()). That's because jQuery separates code for behavior from code for presentation. All the jQuery code will go in the "head." From high up there, it can hook into the DOM using only its patent pending "selectors."

So now let me show you what to add inside the "head" tag in order to load jQuery on the page, and then jQuery code to write that will do all the work.

First, a tangent: Normally, you would download the jQuery.js libraries from jQuery.com onto your own webserver, and serve them to your visitors from there with a "script src" tag. That's fine if you want to do it that way, but there's another option to direct visitors get those libs from Google instead. There are a lot of good reasons for offloading this job, so I leave it to you to read about it. In this example, that's what we're doing.

So, first, add this inside your head tag (before or after title tag) to get your visitor to load the jQuery libraries:

<script src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("jquery", "1");
google.load("jqueryui", "1.5.2");
</script>


Now the browser understands jQuery. All the rest of your jQuery code can come next, in separate script tags, still in head:

<script type="text/javascript">
// When the DOM is ready to have events hook into it...
$(document).ready(function() {

// when the DOM element with id="getrates" is clicked....
$("#getrates").click(function() {

// make that button you clicked disappear...
$(this).hide(); // opposite of show() // See jQueryUI for more info

// and issue an AJAX request to a PHP script in the same directory
// getJSON is a method that expects a JSON-encoded data structure to be returned
// there are other AJAX methods too. See http://docs.jquery.com/Ajax
$.getJSON("script.php", // 1st arg to getJSON is the URI of the script
{
zipcode: $("#zip").val(),
random: "noise"
},
// 2nd arg to getJSON is an array of key-value pairs to send
// to the script. As many as you want.
// left-side is the GET variable name as seen by the target
// script, right-side is the value that will be sent.
// the above 2 args will cause the AJAX script to be hit with
// the query string: ?zipcode=90019&random=noise
// assuming that the user typed "90019" into the
// element on this page with the id="zip"


// 3rd arg is the callback function for the AJAX response
// The script.php responds in a JSON format so jQuery can
// understand the data structure natively, without you
// writing awful parsing of your own:
function(j) {
// erase all OPTIONs from existing select menu on the page
$('#shipping_method options').remove();

// You will rebuild new options based on the JSON response...
var options = '<option value="">Choose Shipping Method</option>';
// "j" is the json object that was output by your PHP script
// it is the array of key-value pairs to turn
// into option value/labels...
for (var i = 0; i < j.length; i++)
{
options += '<option value="' +
j[i].optionValue + '">' +
j[i].optionDisplay +
'</option>';
}
// stick these new options in the existing select menu
$("#shipping_method").html(options);
// now your select menu is rebuilt with dynamic info
}
); // end getJSON
}); // end clicked button to trigger AJAX
}); // end document ready
</script>


That's it. When the user clicks the button, an AJAX request is sent to script.php, and the response is used to rebuild the shipping_method dropdown menu. Prices appear inside the options list before your very eyes. If you want to see a working demo, check here

You may also want to copy this, name it "script.php" and save it on your server in the same directory as the html page above. It outputs a canned JSON answer that the jQuery code will use to make the select options.

<?php
# script.php

$pretend_results = array('PRIORITY_OVERNIGHT' => 39.69,
'STANDARD_OVERNIGHT' => 48.45,
'FEDEX_2_DAY' => 19.75,
'FEDEX_EXPRESS_SAVER' => 15.75);

$haOptions = array();
foreach($pretend_results as $method => $cost)
{
$haOptions[] = array('optionValue' => $method, 'optionDisplay' => "$method $$cost");
}

# make JSON object that will populate select dropdown menu options

if(function_exists('json_encode'))
{
echo json_encode($haOptions); # this puts the php array in the funny javascript array/object
# format so you don't have to know how to translate manually
}

else
{
# some lame web hosts dont have a new version of PHP (5.2+) that includes json functions in core
# so here, I fake it for you so you will have a working demo:
echo '[{"optionValue":"PRIORITY_OVERNIGHT","optionDisplay":"PRIORITY_OVERNIGHT $39.69"},{"optionValue":"STANDARD_OVERNIGHT","optionDisplay":"STANDARD_OVERNIGHT $48.45"},{"optionValue":"FEDEX_2_DAY","optionDisplay":"FEDEX_2_DAY $19.75"},{"optionValue":"FEDEX_EXPRESS_SAVER","optionDisplay":"FEDEX_EXPRESS_SAVER $15.75"}]';
}

# this output is what the jQuery ajax request will receive and parse in its ajax callback function
exit;
?>


When pasting the above PHP script into your editor, if your server does not have the json_encode() function (PHP version < 5.2) then be careful to NOT let your editor (like pico) wrap the long line dummy JSON string with hard line breaks. Hard returns in that data structure will break the fragile thing and your AJAX callback function will not execute.

It is still Javascript, after all, what did you expect?

Sunday, March 02, 2008

Reasonable Backups of Filevault

It doesn't take much web searching to come to the conclusion that the new Time Machine in MacOS 10.5 does not work well with Filevault.

The problem is that to Time Machine, a home directory protected with Filevault is just one big "sparse image" encrypted file. Although it will happily backup this file, doing that defeats one of the purposes of TM, which is to give you snapshots of every individual file from different times, so that you can go back through them and preview them easily before restoring.

If TM is backing up this giant disk image each time, then it is spending all your disk space on your backup drive on the whole disk image for every snapshot. This is a total waste of space. Without Filevault, the behavior would be to take a snapshot only of the changed files, so that your backup drive was only using space to store 1 copy of your files, plus the changes for each snapshot. Another problem with the interaction between FV and TM out of the box is that it's not very convenient in the "Cover Flow" interface to browse through the encrypted images, nor to have to provide a passphrase for each and mount each in order to look at the files inside.

Therefore, I decided to use "rsync" (from Terminal) to backup my home directory to an external drive while I am logged in. In order to keep my files secure on the backup drive, I decided to encrypt that whole device with Truecrypt, which just recently added support for MacOSX.

First I downloaded the Truecrypt .dmg file, mounted that by doubleclicking it, then ran the Truecrypt installer inside there. Once Truecrypt was installed on the Mac, I ran it and told it to encrypt the whole external USB backup drive.

After that was finished, I mounted the new volume according to the "Beginner Tutorial" in the TC documentation. At this time, TC could only create the volume as a FAT filesystem. Because I've been burned before by FAT's maximum filesize of 4G (tarring some stuff directly to the backup drive and having my tarball silently truncated at 4g), I wanted a real filesystem for my backups.

To change the filesystem of the mounted Truecrypt volume, I opened Disk Utilities from the Applications, Utilities menu in the Finder and, while Truecrypt volume is still mounted (so you see it without the encryption), told it to partition the new volume 200G HFS+ and 50G FAT. I left a FAT partition on it so that I could still use the drive on other non-Mac computers.

After the TC volume was re-partitioned and reformated, I was ready to run rsync to copy my home directory in there:
rsync --archive --progress --verbose \
--exclude '.Spotlight-V100' --exclude '.fseventsd' \
--exclude 'Desktop ' --exclude 'Library/*' \
--exclude 'Downloads/*' --exclude 'Music/*'
--exclude 'Public/*' --exclude 'Sites/*'
~me /Volumes/MacBackup/backup
Where my username on the Mac is "me" and the HFS partition inside the Truecrypt volume is "MacBackup" and the directory inside there where I want all my backup stuff is "backup." The result of the command is that everything in my home directory, including hidden files that begin with a '.' like .bashrc, will be copied to the backup directory -- except for a few subdirs of the home that I don't care about and have excluded.

While figuring out which rsync command will work for you, add the "--dry-run " option in until you get it right. I will be saving that command in a shell script that I will periodically execute after connecting the USB drive and running Truecrypt to unlock and mount it.

The reason that I am involving Truecrypt at all is just so that I could use the backup drive on other non-Mac machines, since it is cross platform Windows/Linux/Mac encryption. If I didn't care about the cross platform stuff, I would just have used Apple's Disk Utilities to create an encrypted disk image on the USB drive, and stored backups in there. I sort of defeated some of that purpose by using an Apple-only HFS partition, but maybe in the future there will be a better cross platform filesystem to select from the Disk Utility menu that will also support files larger than 4G.