Ajax-like file submission using hidden iframes

9th January 2014

Only modern browsers have the ability to submit binary data over ajax to a server and even then it is a bit flimsy. You can use a method of posting file inputs to hidden iframes that perform the post submission to the server instead. This works correctly in all browsers that support iframes and file inputs. I have packaged this into a friendly Javascript class:

Javascript

Note: requires jQuery

function HiddenForm(args) {
args = args || {};

var defaults = {
method: "post",
enctype: "application/x-www-form-urlencoded",
action: "",
datatype: "html",
data: {},
done: function() {},
error: function() {},
ready: function() {}
};

for (var key in defaults) {
if (!args.hasOwnProperty(key)) {
args[key] = defaults[key];
}
}

this.args = args;

var targetId = "_hidden_iframe_" + HiddenForm.id++;

this.$div = $('<div class="ui_hidden"></div>');

this.$form = $('<form method="' + args.method +'" enctype="' + args.enctype + '" action="' + args.action + '" target="' + targetId +'"></form>');

this.$iframe = $('<iframe class="ui_hidden" id="' + targetId + '" name="' + targetId + '"></iframe>');
this.$div.append(this.$form, this.$iframe);

this.setData(args.data);

var self = this;
$(document.body).append(this.$div);

this.checkTimer;
}

HiddenForm.prototype.set = function(name, val) {
//this assumes that the data already exists in the form and we are just going to go and change the value now
var $elem = this.$form.find('[name="' + name + '"]');

if (HiddenForm.isFormElement(val)) {
$(val).prop("name", name);
$elem.replaceWidth(val);
} else {
$elem.val(val);
}
};

HiddenForm.prototype.setData = function(data) {
//fill the from with the provided data
for (var key in data) {
if (data.hasOwnProperty(key)) {
this.add(key, data[key]);
}
}
};

//allow the sending on the hidden form
HiddenForm.prototype.send = function() {
if (this.checkTimer) {
window.clearInterval(this.checkTimer);
this.checkTimer = null;
}

var self = this;
this.$iframe.one("load", checkDone);

this.$form.trigger("submit");

//this.$form.trigger("submit");
function checkDone() {
if (self.checkTimer) {
clearInterval(self.checkTimer);
self.checkTimer = null;
}

//read the contents of the iframe and return them as data to the user
var iframeDoc = self.$iframe.get(0).contentWindow.document.documentElement;
var returnObj;

switch (self.args.datatype) {
case "text":
returnObj = iframeDoc.textContent || iframeDoc.innerText;
break;
case "html": default:
returnObj = iframeDoc.innerHTML;
break;
case "json":
try {
returnObj = JSON.parse(iframeDoc.textContent || iframeDoc.innerText);
} catch (e) {
returnObj = false;
}

break;
}

self.args.done(returnObj);
}
};

HiddenForm.id = 0;

HiddenForm.formTags = ["input", "textarea", "select", "option"];

HiddenForm.isFormElement = function(val) {
return (typeof val.tagName == "string" && HiddenForm.formTags.indexOf(val.tagName.toLowerCase())>=0);
};

//let the user add data into the form (key/values)
HiddenForm.prototype.add = function(key, val) {
if (val instanceof jQuery) {
val = val.get(0);
}

//do this rather than check instanceof for maximum compatibility
if (!HiddenForm.isFormElement(val)) {
var $newInput = $('<input type="hidden" />');
$newInput.prop("name", key);
$newInput.val(val);

val = $newInput;
}

$(val).prop("name", key);
this.$form.append(val);
};

HiddenForm.prototype.abort = function() {
var iframeWindow = this.$iframe[0].contentWindow;

if (this.checkTimer) {
clearInterval(this.checkTimer);
this.checkTimer = null;
}

if (iframeWindow) {
if (typeof iframeWindow.stop !== 'undefined') {
iframeWindow.stop();
} else {
iframeWindow.document.execCommand("Stop");
}
}

this.args.error();
};

CSS

The following CSS is also required to 'hide' the elements so everything is behind the scene. Note that the hidden css does not use display:none or visibility:hidden. Using these properties will make the code not function.

.ui_hidden {
position: absolute;
top: -1000px;
left: -1000px;
width: 1px;
height: 1px;
opacity: 0.0;
}

Usage

After inserting the javascript class into a file or script tag and placing the css in the head of the document, you can use the following snippet of Javascript to allow the user to submit a file via the hidden iframe:

//create a file input to use here (could use one already on the DOM)
var $fileInput = $('<input name="files[]" type="file" />');
$(document.body).append($fileInput);

//data contains an object or names and inputs to use in the form
var data = { file: $fileInput };

//the url of the server script to use to do the upload
var targetUrl = "server-side upload script goes here";

var hiddenForm = new HiddenForm({
method: "post",
enctype: "multipart/form-data",
action: targetUrl,
datatype: "json",
data: data,
done: function(data) {
alert("Upload complete");
},
error: function() {
alert("Upload failed");
}
});

Make a comment

Contribute to this article and have your say.