NOTE: Some of this article comes from "Remote Scripting with IFRAME" by Eric
Costello and Apple Developer Connection; 02/08/2002. It can be found here:
http://www.oreillynet.com/pub/a/javascript/2002/02/08/iframe.html.
Forward
In the above-mentioned article, Costello says that "Remote Scripting is the
process by which a client-side application running in the browser and a
server-side application can exchange data without reloading the page. Remote
scripting allows you to create complex DHTML interfaces which interact seamlessly
with your server."
Often, Remote Scripting is asscociated with Microsoft and ASP or with a Java Applet.
I wanted to avoid both, so this is my path. (There is also a popular implementation
here.)
One of my computer science teachers in college was a retired IBM software engineer.
To hear him tell it, when not colating punch cards or replacing vacuum tubes,
he wrote operating system code in 1s and 0s. And now that he was a teacher of Java,
he laughed at some of the "modern benefits and advances", such as code reuse, touted
by the object oriented crowd.
"We had code reuse," he would say. "It was called 'copy and paste'."
In that spirit, I think most programmers would agree that using and incorporating
other people's code, where and when allowed, is a perfectly acceptable and often
helpful practice. Indeed, it's at the heart of open source in general and community
knowledge websites like this one. So much of what I'll be demonstrating in this article
is not of my own creation; the brainpower belongs to others. My job, as is often the
case for web developers, was merely to glue the pieces together, and to "get it working."
The Project
Specifically, what needed to get working was a project at my work that involved porting
a desktop application created in Microsoft Access to a web-based PHP/MySql intranet
application. Among the directives from management was to make the web application look
and behave as much like the existing Access application as possible. And one of the
many small features of this application which didn't map precisely to the web platform
was a self-populating drop down search field. You've certainly seen these in various
applications: the user begins to type in a text box, and as they do, the drop down box
is populated with a list (usually alphabetically ordered), starting with the string that
is being typed. As each letter is typed, the list changes and (usually) becomes more narrowly
focused.
To recreate this in a web page I had several options. First, I could build the Select
element server side with PHP, populating the Option elements from the database, and send
the page with the pre-populated list. I could then tie into that option list with some
Javascript that searched the list and selected the appropriate option based on the string
typed into the text box. Fairly straightforward, except that the option list I needed to
create had more than 60,000 records and was growing by a couple thousand every month. In
fact, a previous developer had prototyped this method in ASP. The resulting page sent to
the browser, with it's 60,000 option-long Select list, was nearly 4 MB. And the Javascript
that searched the list with each "onkeyup" in the textbox was extremely slow, about 5-10
seconds between each keypress, or 30-seconds to type "Smi" and point to the "Smith" records.
Perhaps the Javascript could have been optimized somewhat, but to have a 4 MB page being
requested concurrently, and often, by 10-plus users obviously wouldn't work. So a second
option I had was to omit the Select list and just make the textbox a standard search field.
Type in "Smi", hit enter, and get back a list of all the "Smi*" records.
A reasonable approach, but I felt it didn't meet the "emulate Access" directive closely enough.
So finally, to the subject of the article. I thought, if I could talk to the server from
the client, somehow without reloading the current page, I could "fake" the dynamic drop
down list. Searching the net, I found several Remote Scripting options: several Java applets,
a very nice and well-used Javascript library from Brent Ashley
http://www.ashleyit.com/rs/main.htm,
and other methods. But I settled on the Iframe approach referenced at the beginning of this
article for it's simplicity and lightweight implementation.
The Specifics
You can read Costello's article to get the basics of the technique, but what follows are the
specifics from my case.
First, I created my small HTML form:
<form method="POST" id="searchForm" name="searchForm" action="<?=$_SERVER['PHP_SELF']?>">
Search by Last Name: <input type="text" id="nameSearch" name="nameSearch" onkeyup="doSearch(this.form);"
size="20" value="<?=$nameSearch?>">
<span id="spanSelect">
<select name="id" id="searchSelect">
<option value="-1">Select</option><?=$searchOptions;?></select></span>
<input id="searchButton" type="submit" value="Go" onclick="return submitSearchCheck('searchSelect');" disabled>
<input type="hidden" id="listAsStrng" name="listAsStrng" value="<?=$listAsString;?>">
</form>
I added the reference to the external Javascript file holding the IFrame (and other) code.
<script language="javascript" src="./js/searchForm.js"></script>
Next, I added Eric Costello's IFrame code to the searchForm.js file:
<?php
var IFrameObj;
// our IFrame object - global
function callToServer(term)
{
if (!document.createElement) {return true};
var IFrameDoc;
//var URL = 'server.html' + buildQueryString(theFormName);
var URL = './server.php?s='+term;
if (!IFrameObj && document.createElement)
{
// create the IFrame and assign a reference to the
// object to our global variable IFrameObj.
// this will only happen the first time
// callToServer() is called
try
{
var tempIFrame=document.createElement('iframe');
tempIFrame.setAttribute('id','RSIFrame');
tempIFrame.style.border='0px';
tempIFrame.style.width='0px';
tempIFrame.style.height='0px';
IFrameObj = document.body.appendChild(tempIFrame);
if (document.frames)
{
// this is for IE5 Mac, because it will only
// allow access to the document object
// of the IFrame if we access it through
// the document.frames array
IFrameObj = document.frames['RSIFrame'];
}
}
catch(exception)
{
// This is for IE5 PC, which does not allow dynamic creation
// and manipulation of an iframe object. Instead, we'll fake
// it up by creating our own objects.
iframeHTML='<iframe id="RSIFrame" style="';
iframeHTML+='border:0px;';
iframeHTML+='width:0px;';
iframeHTML+='height:0px;';
iframeHTML+='"></iframe>';
document.body.innerHTML+=iframeHTML;
IFrameObj = new Object();
IFrameObj.document = new Object();
IFrameObj.document.location = new Object();
IFrameObj.document.location.iframe = document.getElementById('RSIFrame');
IFrameObj.document.location.replace = function(location)
{
this.iframe.src = location;
}
}
}
if (navigator.userAgent.indexOf('Gecko') !=-1 && !IFrameObj.contentDocument)
{
// we have to give NS6 a fraction of a second
// to recognize the new IFrame
setTimeout('callToServer()',10);
return false;
}
if (IFrameObj.contentDocument)
{
// For NS6
IFrameDoc = IFrameObj.contentDocument;
}
else if (IFrameObj.contentWindow)
{
// For IE5.5 and IE6
IFrameDoc = IFrameObj.contentWindow.document;
}
else if (IFrameObj.document)
{
// For IE5
IFrameDoc = IFrameObj.document;
}
else
{
return true;
}
IFrameDoc.location.replace(URL);
return false;
}
?>
This code also includes a Javascript function called "buildQueryString" to,
obviously, build a query string, but since I only needed to pass a single
search term I omitted that function, and changed this line:
var URL = 'server.html' + buildQueryString(theFormName);
...to this line:
var URL = './server.php?s='+term;
I also added the "term" parameter to the callToServer() function.
Now, each time a user makes a keystroke in the "nameSearch" textbox, the
doSearch() Javascript function is called. That function looks like this:
<?php
function doSearch(theForm)
{
var searchTerm = theForm.elements['nameSearch'].value;
if(searchTerm.length < 2)
{
return;
}
var s="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ -.";
if(s.indexOf(searchTerm.charAt(searchTerm.length-1)) == -1)
{
return;
}
callToServer(searchTerm);
}
?>
Here, the search value is extracted from the textbox, and if it's less than 2
characters in length, nothing is done, and we are returned. With 60,000
records I needed to limit the search results and the resulting drop down
list, so I implemented this arbitrary length check. Next, I check that
the character entered is alphanumeric, a space, dash, or period. Again,
an arbitrary limit that is specific to my case. Finally, if the search term
is >= 2 characters and conatins valid characters, we dispatch the search term
to callToServer().
Basically, the callToServer() function creates a hidden
IFrame object and "calls"
the server by loading the URL and it's query string with the IFrame's location
attribute. There are also various browser compatibility and error checks interspersed.
Now, over on the server side is our PHP page of course, in my case server.php.
The basic code looks like this:
<?php
if(!isset($_GET['s']))
{
$listAsString = 'No results';
}
else
{
//cleanVar() just does some slash/trim/strip_tags type things
$listAsString = searchFromJs(cleanVar($_GET['s']));
}
function searchFromJs($searchTerm)
{
$query = 'select id, first_name, last_name
from users where last_name like "'.$searchTerm.'%"
order by last_name, first_name";
$result = mysql_query($query);
if($result === false) return 'Invalid query: ' . mysql_error();
if(mysql_num_rows($result) < 1) return 'No results';
while($r = mysql_fetch_assoc($result))
{
$list .= $r['id'].'~'.$r['first_name'].'~'.$r['last_name'].'|';
}
$list = htmlspecialchars(preg_replace('/|$/','',$list),ENT_QUOTES);
return $list;
}
?>
<html>
<script type="text/javascript">
window.parent.handleResponse(encodeURIComponent('<?=$listAsString;?>'));
</script>
</html>
This should be pretty straightforward. I check for the search term, and
call the searchFromJs() funciton if needed. In the searchFromJs() function,
I'm merely building a string containing the database records. The records
are separated by pipes: "|", and the fields are delimited by tildes: "~".
I clean up the string a bit and return it.
The HTML portion of the page is nothing but basic Javascript, so that
when the page is loaded (remember, it's a hidden IFrame), it calls
the handleResponse() Javascript function in it's parent page (search.php).
In the process, it encodes the string. (Not doing this was actually an
undiscovered bug in my application, causing the string to be truncated
in certain cases.)
Let's look at that handleResponse() function now. It looks like this:
<?php
function handleResponse(str)
{
var theString = decodeURIComponent(str);
if(theString == 'No Results' || theString.indexOf('~') == -1)
{
return;
}
var l = new getObj('listAsStrng');
l.obj.value = str;
var list = arrayFromString(theString,'|');
var opts = '';
for(i=0; i < list.length; i++)
{
rec = Array();
rec = list[i].split('~');
opts = opts + '<option value="' + rec[0] +'">' + rec[1] + ' ' + rec[2];
}
var sp = new getObj('spanSelect');
sp.obj.innerHTML = '<select name="names" id="searchSelect">'+opts+'</select>';
var b = new getObj('searchButton');
b.obj.disabled = false;
}
?>
The function takes the string created and passed by searchServer.php,
decodes it, tests it for validity, and transforms it to an array
using this Javascript:
<?php
function arrayFromString( s, delim )
{
var d = (delim == null) ? '|' : delim;
return s.split(d);
}
?>
Back to handleResponse(), the array of names is next transformed
into Option tags, inserted into a Select List, and the list placed
in the HTML document using innerHTML. (The getOBJ() function
merely uses getElementById() to fetch the specified element.)
And that is it. The user now has a dynamic drop down list
wherein they can find and select the appropriate value for
futher processing. Amazingly, even though it is done on every
keystroke (after 2), it is surprisingly fast. Also,
with 60,000+ records, even a subset of records can be quite
large, but I've run into no problems with the string being
too large to pass through Javascript.
As a couple of additional notes, once the user selects a value from the
list, clicks the submit button and the page is reloaded,
the search term and the Select List are de-populated. To
get around this, I added the hidden element "listAsStrng"
to the form, and set its value in handleResponse() here:
<?php
var l = new getObj('listAsStrng');
l.obj.value = str;
?>
Now, when the form is submitted, and the page reloaded, the
search textbox () and the select list ()
can be repopulated with their values, maintaining state for the user.
Also, it's possible, if you can track down the source code
(remember our principle of copy and paste?), to remove the
text box and implement the Select list as a combination
editable text box and drop down list. It's a bit hairy and
browser specific, so I'll leave this as an exercise for the reader.
So, we now have a fairly fluid implementation of
a dynamic drop down using PHP, JavaScript, Remote Scripting,
and a bit of copy and paste. Enjoy.