Classic Web Form POST in AJAX apps
So, you have an AJAX app that has to do a normal POST (not an AJAX post) using a regular web forms for whatever reason? While the solution outline here is not complete (it does not cover doing cross-domain requests), here’s a simple pattern I used to make Ashigaru jQuery plugin work with the Samurai payment gateway.
The problem I was facing was that Herd Hound is a single-page web application. You basically load a single page, and any change on that page is done using JavaScript. All requests to the backend are made via XHR (commonly known as AJAX), and the frontend can only talk to the backend (i.e., it has to talk to the same place from which the JavaScript was loaded). This restriction is known as same origin policy, and it’s been giving pain to anyone wanting to do a mashup.
The same origin policy lead to invention of various techniques to get data from other sites to the AJAX app, including some fancy stuff I’ve only heard about in the newer versions of major browsers. One classic technique is JSONP. This technique has a major drawback in that it cannot be used for sensitive data. Because JSONP requests must be GET requests, any parameters that you want to send must be sent as URL parameters which are not encrypted even over SSL. Another solution was to POST a web form to a hidden iframe using the target attribute.
The target attribute used to be deprecated in HTML 4.01, but in HTML 5, it is no longer deprecated, so you can safely use it knowing that it will be supported for quite some time. What this attribute does is it tells the browser to open the results of the form action in a different target, and not in the current page. The target can be an iframe, so if you have a hidden iframe on your page, the page will not be reloaded, and the user will never know what was loaded in the iframe.
This behavior is what I expoloited for writing Ashigaru.
Basically, you have a form, or you create one using JavaScript, and you add a target attribute that has the same value as the name attribute on the iframe. You also make sure the iframe is hidden. Ashigaru creates a form which has all input elements of type hidden so the form itself is also hiddent. It then takes an object with data that you want to POST, and it populates the form. You can either do that, for a generic solution, or you can use the existing form, and let it submit to the iframe.
Once you submit the form to the iframe, you can use the load event on the iframe to read the data from it once it’s loaded.
Before you attempt to use this technique, here’s a few tips.
- It doesn’t matter what the
actionURL is as long as what ends up in theiframeis coming from your server. Otherwise you cannot access it. - You can access data in the
iframe, butiframecan also access data on the page, so beware of script injection attacks! Make sure there are no embedded malicious scripts. - You cannot prevent scripts in the
iframefrom executing. - If you want to do cross-site requests, you can make a CNAME subdomain that points to that site, and then manipulate
window.document.domainon both the current page and theiframe, and strip out the subdomain parts so the browser thinks it’s the same domain.
Here’s the main function of the Ashigaru:
$.ashigaru = function(data, merchantKey, redirectURI, callback) {
var form;
var fullData = {
// ... defaults
};
$.extend(fullData, data);
var formHTML = '...';
// Replace non-data field placeholders and form attribute placeholders
formHTML = formHTML.replace('$requestURI', samuraiURI);
// ....
// Replace placeholders in form HTML with real data
for (var key in fullData) {
if (fullData.hasOwnProperty(key)) {
formHTML = formHTML.replace('$' + key, fullData[key]);
}
}
// Inject this into DOM
form = $(formHTML);
// Attach the form to body
form.appendTo('body');
// Submit the form
form.unbind('submit');
form.submit(function(e) {
e.stopPropagation();
return createIframe(callback);
});
form.submit();
};
I’ve stripped out a fair bit of error-handling and other little pieces that are not relevant to this post. A few things to note is e.stopPropagation(), adding iframe creation routine into form submit action (so the iframe is created on the fly when the form is submitted), and form.submit() call, which forces form submission (form is not submitted manually by the user).
To be honest, I saw e.stopPropagation() used somewhere in a similar context, and I left it there because it works. I’m slightly ashamed to say that I haven’t tested the code without it. In theory, it will prevent the submit event from being handled by any handlers that are trying to do event delegation. Only the submit action on this particular form will be handled. So I guess that’s why we have it there. This should be a completely isolated form, created for the purpose of making this method of POSTing work, so we don’t want anyone else on the page reacting to the event.
Another thing I did was first make sure submit event is not handled by anything other than my code. For the same reason as e.stopPropagation(), obviously.
Here’s the createIframe method:
function createIframe(callback) {
var iFrame = $('<iframe src="" name="samurai-iframe" '+
'style="display:none">' +
'</iframe>');
// Attach iframe to <body>
iFrame.appendTo('body');
// Do the right thing when iframe loads
iFrame.load(function() {
onResultLoad(this, callback);
removeForm();
});
return true;
}
Fairly simple. It creates the HTML for the iframe, attaches it to body, and adds the load event handler whic extracts the data from the iframe and deletes the form. I’m not going to show you the code that removes the form. It’s not relevant. You can check out the Ashigaru source if you want to know more.
There’s one thing to note here, and that’s the fact that we return true as the last thing in the createIframe method. Remember that we said return createIframe(callback) in the form’s submit handler? Well, this is the thing. If you return false in a handler, the event will be prevented from happening, and the form will not submit. I haven’t written the code yet, but I will do basic sanity check: see if the iframe was created successfully, and return false if it wasn’t (so that the form is not submitted).
I’ll just show you one last snippet, and that’s the onResultLoad handler:
function onResultLoad(iFrame, callback) {
var resultDocument = $(iFrame).contents();
var jsonData;
// Remove the form
removeForm();
// The result document should contain JSON data in a <script> tag
jsonData = $.trim(resultDocument.find('body script').text());
}
To get the DOM tree of iframe contents, you can use the contents() method. It will return a jQuery object that you can use to access the DOM inside the iframe. Note that this only works if the iframe satisifes the requirements of the same-origin policy. The reason this method works for Ashigaru is that Samurai redirects the user back to a page on my server, so the final result of making the POST request always satisfies the same-origin policy.
One you have access to the iframe’s DOM, it’s a matter of doing your usual $(..) to get whatever you want from it.
Specifically in Ashigaru, I have a single <script> tag that contains JSON data enclosed in round brackets:
<script>({"status": "ok"})</script>
The round brackets are there to prevent the browser from thowing SyntaxError exceptions because the object literal is required to be inside brackets.




