So far it's not too painful. All that's left is the client-side JavaScript to handle the process. Of course, this is where all the work is done. (Well...there is server-side code, but it is not covered. You can use any scripting language to return the HTML described on the Remote Data Format page previously.)
We want to make this method look identical to using the xmlHttpRequest object. This way code can be written later that attempts to use the XHR object and transparently falls back to using an iframe when needed. The API for the RPC can be shared. To do this, the iframe is extend by adding properties and methods that match the XHR object's methods and properties plus a few others to glue it all together.
There are some extra elements in the code to work with the IE activeX XmlHttpRequest object. These are not needed for RPCs with iframes. They are there so the two methods can be combined letting the code automatically and transparently pick the method based on browser support.
Unlike the original code shown for remote scripting with and iframe, which created the iframe at the time of the RPC, this method creates the iframe at the time the page is loaded. Two functions are involved: createIframe and createExtIframe. Function createIframe, called in createExtIframe, is responsible for actually creating an iframe. The function createExtIframe is responsible for extending the the iframe once it is created to match the xmlHttpRequest object.
Properties and methods are added simply by initializing them—an advantage of object oriented languages that are prototype based over class base ones. I like to initialize all properties up front even though they can be added at any time. This way there isn't a problem with missing properties later. Here is the code that handles this.
var HTTPObj = null; //hidden iframe or xmlHttpRequest object var HTTPtarget = null; //This global is to accommodate another method addEvent(window, "load", createXMLHttp); function createExtIframe() { /* Create Iframe if needed */ var iframeObj = document.getElementById("rs-iframe"); if (!iframeObj) { iframeObj = createIframe(); } /* Now that we have an iframe extend it */ if (iframeObj) { iframeObj.backbutton = false; iframeObj.RPCType = "iframe"; iframeObj.targetId = ""; iframeObj.status = 0; iframeObj.nexturl = ""; iframeObj.responseText = ""; try { iframeObj.readyState = 0; } catch (e) { /* IE already has this property as a * read only text with the values of * uninitialized, loading, loaded, interactive, and complete * so we just catch the error and go on. */ } iframeObj.callBack = callb; iframeObj.open = openMethod; iframeObj.send = sendMethod; iframeObj.iwindow = null; /* IE has an onreadystatechange event, * but initializing it is not a issue */ iframeObj.onreadystatechange = ""; } else { iframeObj = null; } HTTPObj = iframeObj; }//eof createExtIframe
addEvent is a user function that binds createXMLHttp to the window onload event and is discussed in the article Adding an Event Handler.
createExtIframe, is straight forward JavaScript. It calls for the iframe to be created if it doesn't exist and then adds several properties and methods by assignment. A try-catch error trap is used to handle iframes that have a readOnly readyState property (IE for example). An error is thrown when the assignment is made, which is ignored by trapping it and the code continues running. The last step assigns the extended iframe to the global variable HTTPObj.You may recognize some of the new properties and methods as xmlHttpRequest object properties and there are a couple of extras. Let's look at the them.
There is a test for the iframe because it maybe necessary
to code it in the HTML page to support some browsers. Only one line is required:
<div id="iframe-div><iframe id="rs-iframe"></iframe></div>.
The following methods are added.
Here is the createIframe code.
function createIframe() { /* Routine to create hidden iframe * Discovered that wrapping the iframe in * a div with display none works well in * most, if not all, common browsers. * * Note this test iframeObj.nodeType == "undefined" * is to trap some browsers that create a useless iframe. * For example Konqueror 3.2 and earlier. */ var iframeObj = null; try { //Create Iframe and put it at bottom of page var tmpFrame; //IE 6.0 fix using wrapper div with display none var tmpDiv = document.createElement("div"); tmpDiv.setAttribute("id", "iframe-div"); tmpFrame = document.createElement("iframe"); tmpFrame.setAttribute("id", "rs-iframe"); tmpFrame.setAttribute("name", "rs-iframe"); if (typeof document.body.getAttribute("className") == 'string') tmpFrame.setAttribute("className", "hidden-frame"); else tmpFrame.setAttribute("class", "hidden-frame"); tmpDiv.appendChild(tmpFrame); document.body.appendChild(tmpDiv); if (typeof document.frames != "undefined") { /* Required for IE 5 on Mac and throws error on IE 5.0 PC * which we need to branch to a different process. */ iframeObj = document.frames["rs-iframe"]; } if (!iframeObj || typeof iframeObj.nodeType == "undefined") { /* Most browsers yield null or good iframe reference, * but some return an object with no properties so nodeType test. */ iframeObj = document.getElementById("rs-iframe"); } } catch (e) { /* Hack to handle IE 5.0 on PC, which doesn't want to 'automate' * adding an iframe. * * Reguired: * 1. HTML string for iframe and NOT an element object. * 2. use iframe src attribute to load first RPC; otherwise only subsequent calls work * 3. A block container to hold iframe * 4. Attach container and iframe to any block inside body but NOT the body. */ /* Put iframe in container (innerHTML must be used) * and append to any div in page (this may exist from above) */ var iframeHTML='<iframe id="rs-iframe" name="rs-iframe" class="hidden-frame"><\/iframe>'; if (!document.getElementById("iframe-div")) { tmpDiv = document.createElement("div"); tmpDiv.setAttribute("id", "iframe-div"); tmpDiv.innerHTML = iframeHTML; document.getElementsByTagName('DIV')[0].appendChild(tmpDiv); } else { document.getElementById("iframe-div").innerHTML = iframeHTML; } iframeObj = document.getElementById("rs-iframe"); } /* How did we do? Return if alternate page should load */ if (iframeObj && typeof iframeObj.nodeType == "undefined") { iframeObj = null; } return iframeObj; }//eof createIframe
The in-code comments seem sufficient to explain the steps in creating the iframe. So there is no need to say anymore. Now we should take a look at the code for the methods added to the iframe.
function callb(txt) { try { this.readyState = 4; } catch (e) { /* IE already has this property as a * read only text with the values of * uninitialized, loading, loaded, interactive, and complete * so we just catch the error and go on. */ } this.status = 200; this.responseText = txt; this.onreadystatechange(); }//eof callb function openMethod(method, url, asyc) { /* The method argument is ignored, * but is here to match the prototype * of the open method of the * xmlHttpRequest object */ this.nexturl = url; this.status = 0; try { this.readyState = 0; } catch (e) { /* IE already has this property as a * read only text with the values of * uninitialized, loading, loaded, interactive, and complete * so we just catch the error and go on. */ } }//eof openMethod function getWindowHandle(iframeObj) { var iframeWin = null; /* Get handle to iframe window */ if (iframeObj.contentWindow) { // IE5.5+, Mozilla, NN7 iframeWin = iframeObj.contentWindow; } else if (iframeObj.contentDocument) { // NN6, Konqueror iframeWin = iframeObj.contentDocument.defaultView; } else if (iframeObj.Document) { // IE5 iframeWin = iframeObj.Document.parentWindow; } return iframeWin; } //eof get WindowHandle function sendMethod() { this.iwindow = getWindowHandle(this); if (this.iwindow) { if (this.backButton) { // This puts the url in history and // preserves the back button this.iwindow.location = this.nexturl; } else { //This replaces the history entry "breaking" the back button. this.iwindow.location.replace(this.nexturl); } } }//eof sendMethod
Notice that callb, which is assigned to callBack, populates readyState and status with defaults for a finished and successful HTTP request. The openMethod, which is assigned to open, is used to reset them at the beginning of each RPC. The returned string data is stored in responseText and then onreadystatechange is called. This looks just like the xmlHttpRequest object to the response handler, which is assigned to onreadystatechange. Onreadystatechange is an iframe event in some versions of IE. Consequently, the handler will be triggered early. It should ignore calls until status is 200 when the responseText variable has been set.
The send method uses the replace method of the iframe's window location object accessed through the iwindow property added to the iframe. Using the replace method keeps all but one url out of the history; thus, the back button won't step through every transaction.
IE 5.0 requires that the iframe's window reference be regenerated everytime a new page is loaded into the iframe. Otherwise, it will crashes on subsequent RPCs. For other browsers iwindow can be set in createExIframe when all the other properties are initialized.
From here on, the JavaScript is the same script used for RPC with the xmlHttpRequest object. We have the onclick event handler, do_rpc, a stock onreadystatechange event handler, and a couple of utility functions for formating the URL.
function do_rpc(url, target_elem, data, handler) { /* 1. test if the browser has the necessary * features for the RPC and to handle the response data. * 2. Do any preprocessing for the URL * e.g. assembling a query string from form fields * 3. Make call to server * 4. return if alternate page in form action * or link's href should load */ var doDefault = true; if (HTTPObj) { /* Store ID of block receiving results. * This try-catch code block isn't needed for * RPCs using iframes. It is here because we * want to be able to use this with the xmlHttpRequest * method where it is needed for IEs activeX. */ try { HTTPObj.targetId = target_elem; } catch (e) { HTTPtarget = target_elem; } var formattedURL = formatURL(url, data); //Needed for IE later HTTPObj.open("GET", formattedURL, true); HTTPObj.onreadystatechange = handler; HTTPObj.send(null); doDefault = false; } return doDefault; }//eof function handleResponse() { if (HTTPObj.readyState == 4 || HTTPObj.readyState == "complete" ) { if (HTTPObj.status == 200) { var id = (HTTPObj.targetId) ? HTTPObj.targetId : HTTPTarget; var targ = document.getElementById(id); targ.innerHTML = HTTPObj.responseText; } else if (HTTPObj.readyState != "complete") { //handle error for use with xmlHttpRequest } } } //eof handleResponse function getRPCType() { /* This is so the server side * code can support different RPC * methods from different platforms * We tell it what type of RPC is * being made. */ var RPCType = null; if (HTTPObj.RPCType) { RPCType = HTTPObj.RPCType; } else { /* For IE Active X */ RPCType = "ajaxg"; } return "type=" + RPCType; }//eof getRPCType function formatURL(url, data) { /* Assemble query string and append to url * * If no HTTObj.type then this is being used * with IEs ActiveX xmlHttpRequest object. * Not and issue when just using iframes. * Type indicates to the server code * if iframe or XMLHttpRequest. The server code * is the same for both processes. */ var type = getRPCType(); if (data) { switch (typeof data) { case "object": //Get string from form like method get if (data.tagName.toLowerCase() == "form") { url += form2query(data); } url += "&" + type; break; case "string": //formated string supplied if (!/^[?]/.test(data) ) { data = '?' + data; } url += data + "&" + type; break; default: url += "?" + type; //use what is passed with url } } else { url += "?" + type; } return url; }//eof formatURL function form2query(frm) { /* To string together fieldname * value pairs from form elements * with name property set. * * Format ?name=value&name=value ... */ var qry = ""; //final query string var pair = "?"; //format one name/value pair var field; //form field being processed for (var i = 0; i < frm.elements.length; i++) { field = frm.elements[i]; if (typeof field.name != "undefined" && field.name != "") { switch (field.type) { case "select-one": pair += field.name + "=" + field.options[field.selectedIndex].value; break; case "radio": case "checkbox": if (field.checked) { pair += field.name + "=" + encode_str(field.value); } break; default: pair += field.name + "=" + encode_str(field.value); } if (pair.length > 1) { /* Test in case first element * is unchecked radio or checkbox */ qry += pair; pair = "&"; } } } return qry; }//eof form2query function encode_str(strg){ if (window.encodeURIComponent) return encodeURIComponent(strg); else if (window.escape) return escape(strg); else return strg.replace(/\s/g, "+"); }//eof encode_str
The method handleResponse that is shown is bare bones for demonstration. The examples on the following pages include versions coded to work in the examples.
Handling the RPC type and formatted url are complicated by the need to remain compatible with IE's ActiveX xmlHttpRequest object. A property cannot be added to an activeX object. The code shown walks around some pitfalls.
The last functions shouldn't require any more explaination so we are done with the JavaScript