New geek bits ?
IT ideas, solutions, designs ... gosh, possibly even actual code and software.
You can click the green links at the right to view different subjects. The freshest content is always displayed first, so if what you immediately see isn't new to you, check back later.
If you haven't already read it, check out Serendipitous SQL.
File uploading from a web page is straightforward; add a file input control, a submit button, and you're done. Uploading multiple files at once is a little more challenging.
I've just redeveloped an email composition form for a website, adding rich text and multiple attachments. There were two ways I could have implemented the attachment handling.
The first method is multiple submissions. Each time you add a file it's submitted, saved on the server, and the page refreshes to list the file with a REMOVE button alongside it. A lot of webmail clients work this way, and it does have the advantage of avoiding mandatory javascript.
The second option is to use javascript on the client. This feels like a better user experience as there's no delay until the SUBMIT button is clicked - although of course uploading takes the same amount of time regardless of when it happens. In electing to use this option I had to ensure the functionality degraded gracefully for browsers with scripting turned off. To be honest, I've not seen any for some years, but it's good practice.
I'm going to break this into three parts. The first is an overview, the second covers the HTML and Javascript comes third. Ready?
OK. Here's what the control looks like - it's live, so have a play.
|
Add an attachment:
|
It's not the prettiest widget in the world, but we'll talk about ways to improve the cosmetics later. The important thing for now is that it works. And how does it work? Smoke and mirrors.
When the file input control's specifications were written, care was taken to ensure hackers couldn't use it inappropriately. It's difficult to style and even more difficult to manipulate. You click the BROWSE button, the file dialog appears. Select a file, click the dialog's OPEN button and the file path and name are displayed in the file input control. That's it.
But like all controls, the file input has events. When a file is selected, the onchange event is fired. We're responding to that event by making the control invisible, displaying its filename with a REMOVE link alongside it, and replacing the original control with a brand new one.
That's right. By the time you have four files listed in the above example, you have five file input controls on the form. That's how it's done.
Let's move on and take a look at the HTML, stripped back to basics.
<div id="attachments"" style="display: none;"></div> <table> <tbody> <tr> <td> Add an attachment: <div id="attachselect"> <input type="file" name="file[]" id="attachfile-0" onchange="changeAttach(this.id);" /> </div> </td> <td id="attachcell" rowspan="2"></td> </tr> </tbody> </table>
First things first: I realise that using a table for layout puts me in a state of sin. Certainly you shouldn't emulate my example, but don't feel you need to counsel me. This is just an example, and I already have all the counselling I can eat.
Note the attachselect div which always contains the current file input control, the invisible attachments div which will contain file input controls once they're populated, and the attachcell table cell where we'll display the selected filenames and their individual REMOVE buttons.
The initial attachfile-0 file input control is the first in a dynasty, and its onchange event is the key to how we manage the succession. Note the file input control's name, file[]. Using the same name attribute for all the file input controls and putting square brackets at the end of that name means that when the form is submitted, all the file input control values will be treated as an array. This simplifies handling on the server.
On to the Javascript now? Let's start with the logic, using verbose variable names for clarity.
function changeAttach(sID) {
var oCurrentFileInput = document.getElementById(sID);
var oCurrentFileInputHolder = document.getElementById('attachselect');
var oHiddenStorage = document.getElementById('attachments');
var oFileNamesDisplay = document.getElementById('attachcell');
// Only continue if we have all the above elements.
if (oCurrentFileInput && oCurrentFileInputHolder && oHiddenStorage && oFileNamesDisplay) {
// Get the current file index.
var iCurrentID = getIndexFromString(sID);
// Move the current file input into our hidden div ...
if (oHiddenStorage.appendChild(oCurrentFileInput)) {
// ... and replace it with a new file input element.
oCurrentFileInputHolder.appendChild(createFileInput(iCurrentID + 1));
// Finally display the file and the REMOVE link in a new div.
oFileNamesDisplay.appendChild(createDiv(iCurrentID, oCurrentFileInput.value));
}
}
}
First we make sure we can get the four elements essential to the process: the current file input control, the div holding the current file input control, the hidden div where we'll store previous file input controls, and the table cell where we'll display selected filenames. If any these aren't accessible we just exit ... the user will still be able to upload a single file.
Next we invoke the helper routine getIndexFromString() to parse the input value for the current file input index number.
Now we're on the home straight. We move the current file input control to its new home in the hidden storage div. As it can't be in two places at once, this leaves its previous location empty. We fill that with a new file input control returned by the helper routine createFileInput(), and then the helper routine createDisplayDiv() gives us the filename and an associated REMOVE button to display.
function getIndexFromString(sID) {
// sID is in the form 'attachfile-n'
// where n is an integer greater than -1.
var iPos = sID.indexOf('-');
return parseInt(sID.substr(iPos + 1));
}
function createFileInput(iID) {
// Create a new form element for the next file and
// name it with the new index.
var oFileInput = document.createElement('input');
oFileInput.setAttribute('type', 'file');
oFileInput.setAttribute('name', 'file[]');
oFileInput.setAttribute('id', 'attachfile-' + iID);
oFileInput.setAttribute('size', '30');
oFileInput.onchange = function(){changeAttach(this.id);};
return oFileInput;
}
function createDisplayDiv(iID, sFile) {
var oDiv = document.createElement('div');
oDiv.setAttribute('id', 'divfile-' + iID);
// Strip the path, otherwise IE displays the entire path
// and Firefox displays just the filename.
var iPos = sFile.lastIndexOf("\\");
if (iPos > -1) sFile = sFile.substr(iPos + 1);
oDiv.innerHTML = '<a href="#" ' +
'onclick="javascript:removeAttach(\'file-' +
iID + '\'); return false;" ' +
'style="font-size: 0.8em;">REMOVE</a> ' +
sFile;
return oDiv;
}
If the browser has scripting turned off, none of this is going to happen. That just means the user can't select additional files. But a single file can still be selected and submitted: it's a sufficiently elegant degradation.
The helper routines should be self-explanatory. Here's the code.
In createDisplayDiv() we take account of a significant difference between the value attribute returned by Internet Explorer and Firefox. IE will give you the complete path and filename, Firefox just the filename. If you don't mind the display being different on different browsers, you can omit the tidy-up I've implemented.
As with the use of tables for layout, you should probably avoid the innerHTML method if you want to skip additional time in purgatory once your coding days are done.
The final piece of Javascript is a very simple routine that's invoked if the user decides to dump a previously-selected file by clicking its REMOVE button.
function removeAttach(sID) {
var oThisDisplayDiv = document.getElementById('div' + sID);
var oThisFileInput = document.getElementById('attach' + sID);
var oFileNamesDisplay = document.getElementById('attachcell');
var oHiddenStorage = document.getElementById('attachments');
// If we have all the necessary elements, remove both the
// file input element and the display row from the table.
if (oThisDisplayDiv && oThisFileInput &&
oFileNamesDisplay && oHiddenStorage) {
if (oHiddenStorage.removeChild(oThisFileInput)) {
oFileNamesDisplay.removeChild(oThisDisplayDiv);
}
}
}
Very simply, we get the div holding the file name and REMOVE button, the associated file input control, and their respective parents. We then cast the filename display and the file input control into the outer darkness - that's it.
When the form containing these controls is submitted, the file details will be passed as a multidimensional array. Because the form will always have one empty file input control when its submitted, the last file referenced by the array will be an empty value - it's important to check each file before processing it.
Near the start of this essay I mentioned possible improvements to the cosmetic appearance of the input file control. There are several possibilities, all of them have pitfalls.
If you only want to support browsers later than Internet Explorer 5.01, Michael O'Grady's technique can work well. It's explained in detail with code examples in this Quirksmode article. There's a similar solution by Shaun Inman which is worth a look, and for completeness don't miss this article by Burhan Khan.
When uploading multiple files using the technique I've outlined, the text box part of the file input control is effectively redundant. Using the opacity techniques outlined in the above three articles to hide that part of the control altogether and re-style the BROWSE button is probably a good approach.
Leigh Harrison is currently repaying karma from a past life by working as an IT Generalist in this one.