Tuesday, July 25, 2006

Dynamically add/subtract upload fields to form: Chapter 2, the backend

In the previous chapter, we covered what it takes to build XHTML-compliant dynamic content inside a webpage. One of the things we did not do was use some kind of input element name iteration. We don't need to.

Let's reconsider the way the name attribute behaves in HTML forms. While it's true that XHTML deprecates name in favor of id in most situations, inside forms name still serves a critical function: defining a collection of noncounted elements.

Forms are all about collections: checkboxes and multiple selections inside a select box send collections to CGIs. When multiple checkboxes inside a form share the same name, they send their values as a collection. Checkboxes are input elements, and so are file uploads. In other words, an array.

Lincoln Stein's CGI Perl module is capable of handling these arrays, and has been since version 2.47. The mental block here is assuming that CGI's param method only returns scalars. Fortunately, it doesn't. The problem, however, is that Stein's own documentation says the following:
To be safe, use the upload() function (new in version 2.47). When called with the name of an upload field, upload() returns a filehandle, or undef if the parameter is not a valid filehandle. In an list context, upload() will return an array of filehandles. This makes it possible to create forms that use the same name for multiple upload fields. This is the recommended idiom.
This would be beautiful, if it were true. Unfortunately, it is not. param() returns lists, upload() only returns the first member of the list passed to it. I mention this here because forum posts indicate this has been a land mine for many since upload() was introduced a few years back. So, if all the potential upload inputs have a name attribute of 'mypix', the following neatly harvests them:
@mypictures = param('mypix');
The array sent to param() is merely the upload filenames as filehandles -- to which various methods return metadata for preprocessing. uploadInfo($file)->{'Content-Type'} returns the MIME-type, tmpFileName($file) returns the path to the actual file on the server, and there are others you'll see.

First, a bit of housecleaning. The array param() returns is obedient and dumb; it will contain blank elements for every unused input field or those filled with nonsense text instead of filepaths. We can neatly purge these with a single statement:
@mypictures = grep {$_} @mypictures;
The expression tells Perl to return the results of a regex search against non-null members in @mypictures. No second temporary array, no loops. $_ can be your friend. (FYI: your browser is the one purging the nonsense fields, not CGI.pm)

Dynamically add/subtract upload fields to form: Chapter 1, the frontend

The assignment: create a webpage which permits faculty to upload digital course reserves materials, using XHTML frontend and Perl backend, then attach those uploads to an email sent to a clearinghouse mailbox.

The complications:
  1. backend needs to be agnostic towards how many uploads are being sent, preferably no iteration over numbered input name attributes
  2. frontend is proper XHTML, therefore adding new input elements can't use document.write JavaScript methods

Let's deal with the frontend first. XHTML deprecates document.write because it breaks the idea of XHTML as a properly formed XML document: anything could be inserted into your webpage no matter how well or poorly formed it is. If I'm not mistaken XHTML served as its correct MIMEtype results in browsers throwing errors when they encounter document.write.

Fortunately, XHTML nominally being XML, it supports XML's methods for inserting, removing and changing internal content. Let's take a look at some sample JavaScript: