Search the Catalog
JavaScript Cookbook

JavaScript Cookbook

By Jerry Bradenbaugh
1st Edition October 1999 (est.)
1-56592-577-7, Order Number: 5777
500 pages, $34.95 (est.)

Chapter 1 The Client-Side Search Engine

Application Features

  • Efficient Client-Side Searching
  • Multiple Search Algorithms
  • Sorted and Portioned Search Results
  • Scalable to Thousands of Records
  • Easily Coded for JavaScript 1.0 Compatibility

JavaScript Techniques

  • Using Delimited Strings to Store Multiple Records
  • Nested for Loops
  • Wise Use of document.write()
  • Using the Ternary Operator for Iteration

Every site could use a search engine, but why force your server to deal with all those queries? The Client-Side Search Engine allows users to search through your pages completely on the client side. Rather than sending queries to a database or application server, each user downloads the "database" within the requested web pages. This makeshift database is simply a JavaScript array. Each record is kept in one element of the array.

This approach has some significant benefits, chiefly reducing the server's workload and improving response time. As good as that sounds, keep in mind that this application is restricted by the limitations of the user's resources, especially processor speed and available memory. Nonetheless, it can be a great utility for your web site. You can find the code for this application in the ch01 folder of the zip file. Figure 1-1 shows the opening interface.

Figure 1-1. The opening interface


This application comes equipped with two Boolean search methods: AND and OR. You can search by document title and description, or by document URL. User functionality is pretty straightforward. It's as easy as entering the terms you want to match, then pressing Enter. Here's the search option breakdown:

TIP: Don't forget your zip file! As noted in the preface, all the code used in this book is available in a zip file on the O'Reilly site. To grab the zip, go to

Figure 1-2 shows the results page of a simple search. Notice this particular query uses the default (no prefixes) search method and "javascript" as the search term. Each search generates on the fly a results page that displays the fruits of the most recent search, followed by a link back to the help page for quick reference.

Figure 1-2. A typical search results page


It's also nice to be able to search by URL. Figure 1-3 shows a site search using the url: prefix to instruct the engine to search URLs only. In this case the string html is passed, so the engine returns all documents with html in the URL. The document description is still displayed, but the URL comes first. The URL search method is restricted to single-match qualification, just like the default method. That shouldn't be a problem, though. Not many people will be eager to perform complex search algorithms on your URLs.

Figure 1-3. Results page based on searching record URLs


This application can limit the number of results displayed per page and create buttons to view successive or previous pages so that users aren't buried with mile-long displays of record matches. The number displayed per page is completely up to you, though the default is 10.

Execution Requirements

The version of the application discussed here requires a browser that supports JavaScript 1.1. That's good news for people using Netscape Navigator 3 and 4 and Microsoft Internet Explorer 4 and 5, and bad news for IE 3 users. If you're intent on backwards compatibility, don't fret. I'll show you how you can accommodate IE 3 users (at the price of functionality) in the "Potential Extensions" section of this chapter.

All client-side applications depend on the resources of the client machine, a fact that's especially true here. It's a safe bet the client will have the resources to run the code, but if you pass the client a huge database (more than about 6,000 or 7,000 records), your performance will begin to degrade, and you'll eventually choke the machine.

I had no problem using a database of slightly fewer than 10,000 records in MSIE 4 and Navigator 4. Incidentally, the JavaScript source file holding the records was larger than 1 MB. I had anywhere between 24 and 128 MB of RAM on the machine. I tried the same setup with NN 3.0 Gold and got a stack overflow error--just too many records in the array.

On the low end, the JavaScript 1.0 version viewed with MSIE 3.02 on an IBM ThinkPad didn't allow more than 215 records. Don't let that low number scare you. The laptop was so outdated you could hear the rat on the exercise wheel powering the CPU. Most users will likely have a better capacity.

The Syntax Breakdown

This application consists of three HTML files (index.html, nav.html, and main.html ) and a JavaScript source file (records.js). The three HTML files include a tiny frameset, a header page where you enter the search terms, and a default page in the display frame with the "how-to" instructions.


The brains of the application lie in the header file named nav.html. In fact, the only other place you'll see JavaScript is in the results pages manufactured on the fly. Let's have a glimpse at the code. Example 1-1 leads the way.

Example 1-1: Source Code for nav.html

  1. <HTML>
  2. <HEAD>
  3. <TITLE>Search Nav Page</TITLE>
  5. <SCRIPT LANGUAGE="JavaScript1.1" SRC="records.js"></SCRIPT>
  6. <SCRIPT LANGUAGE="JavaScript1.1">
  7. <!--
  9. var SEARCHANY = 1;
  10. var SEARCHALL = 2;
  11. var SEARCHURL = 4;
  12. var searchType = "";
  13. var showMatches = 10;
  14. var currentMatch = 0;
  15. var copyArray = new Array();
  16. var docObj = parent.frames[1].document;
  18. function validate(entry) {
  19. if (entry.charAt(0) == "+") {
  20. entry = entry.substring(1,entry.length);
  21. searchType = SEARCHALL;
  22. }
  23. else if (entry.substring(0,4) == "url:") {
  24. entry = entry.substring(5,entry.length);
  25. searchType = SEARCHURL;
  26. }
  27. else { searchType = SEARCHANY; }
  28. while (entry.charAt(0) == " ") {
  29. entry = entry.substring(1,entry.length);
  30. document.forms[0].query.value = entry;
  31. }
  32. while (entry.charAt(entry.length - 1) == " ") {
  33. entry = entry.substring(0,entry.length - 1);
  34. document.forms[0].query.value = entry;
  35. }
  36. if (entry.length < 3) {
  37. alert("You cannot search strings that small. Elaborate a little.");
  38. document.forms[0].query.focus();
  39. return;
  40. }
  41. convertString(entry);
  42. }
  44. function convertString(reentry) {
  45. var searchArray = reentry.split(" ");
  46. if (searchType == (SEARCHALL)) { requireAll(searchArray); }
  47. else { allowAny(searchArray); }
  48. }
  50. function allowAny(t) {
  51. var findings = new Array(0);
  52. for (i = 0; i < profiles.length; i++) {
  53. var compareElement = profiles[i].toUpperCase();
  54. if(searchType == SEARCHANY) {
  55. var refineElement = compareElement.substring(0,
  56. compareElement.indexOf('|HTTP'));
  57. }
  58. else {
  59. var refineElement =
  60. compareElement.substring(compareElement.indexOf('|HTTP'),
  61. compareElement.length);
  62. }
  63. for (j = 0; j < t.length; j++) {
  64. var compareString = t[j].toUpperCase();
  65. if (refineElement.indexOf(compareString) != -1) {
  66. findings[findings.length] = profiles[i];
  67. break;
  68. }
  69. }
  70. }
  71. verifyManage(findings);
  72. }
  74. function requireAll(t) {
  75. var findings = new Array();
  76. for (i = 0; i < profiles.length; i++) {
  77. var allConfirmation = true;
  78. var allString = profiles[i].toUpperCase();
  79. var refineAllString = allString.substring(0,
  80. allString.indexOf('|HTTP'));
  81. for (j = 0; j < t.length; j++) {
  82. var allElement = t[j].toUpperCase();
  83. if (refineAllString.indexOf(allElement) == -1) {
  84. allConfirmation = false;
  85. continue;
  86. }
  87. }
  88. if (allConfirmation) {
  89. findings[findings.length] = profiles[i];
  90. }
  91. }
  92. verifyManage(findings);
  93. }
  95. function verifyManage(resultSet) {
  96. if (resultSet.length == 0) { noMatch(); }
  97. else {
  98. copyArray = resultSet.sort();
  99. formatResults(copyArray, currentMatch, showMatches);
  100. }
  101. }
  103. function noMatch() {
  105. docObj.writeln('<HTML><HEAD><TITLE>Search Results</TITLE></HEAD>' +
  108. '<FONT FACE=Arial><B><DL>' +
  109. '<HR NOSHADE WIDTH=100%>"' + document.forms[0].query.value +
  110. '" returned no results.<HR NOSHADE WIDTH=100%>' +
  111. '</TD></TR></TABLE></BODY></HTML>');
  112. docObj.close();
  113. document.forms[0];
  114. }
  116. function formatResults(results, reference, offset) {
  117. var currentRecord = (results.length < reference + offset ?
  118. results.length : reference + offset);
  120. docObj.writeln('<HTML><HEAD><TITLE>Search Results</TITLE>\n</HEAD>' +
  123. '<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP>' +
  124. '<FONT FACE=Arial><B>Search Query: <I>' +
  125. parent.frames[0].document.forms[0].query.value + '</I><BR>\n' +
  126. 'Search Results: <I>' + (reference + 1) + ' - ' +
  127. currentRecord + ' of ' + results.length + '</I><BR><BR></FONT>' +
  128. '<FONT FACE=Arial SIZE=-1><B>' +
  129. '\n\n<!-- Begin result set //-->\n\n\t<DL>');
  130. if (searchType == SEARCHURL) {
  131. for (var i = reference; i < currentRecord; i++) {
  132. var divide = results[i].split('|');
  133. docObj.writeln('\t<DT>' + '<A HREF="' + divide[2] + '">' +
  134. divide[2] + '</A>\t<DD><I>' + divide[1] + '</I><P>\n\n');
  135. }
  136. }
  137. else {
  138. for (var i = reference; i < currentRecord; i++) {
  139. var divide = results[i].split('|');
  140. docObj.writeln('\n\n\t<DT>' + '<A HREF="' + divide[2] + '">' +
  141. divide[0] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>');
  142. }
  143. }
  144. docObj.writeln('\n\t</DL>\n\n<!-- End result set //-->\n\n');
  145. prevNextResults(results.length, reference, offset);
  146. docObj.writeln('<HR NOSHADE WIDTH=100%>' +
  147. '</TD>\n</TR>\n</TABLE>\n</BODY>\n</HTML>');
  148. docObj.close();
  149. document.forms[0];
  150. }
  152. function prevNextResults(ceiling, reference, offset) {
  153. docObj.writeln('<CENTER><FORM>');
  154. if(reference > 0) {
  155. docObj.writeln('<INPUT TYPE=BUTTON VALUE="Prev ' + offset +
  156. ' Results" ' +
  157. 'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
  158. (reference - offset) + ', ' + offset + ')">');
  159. }
  160. if(reference >= 0 && reference + offset < ceiling) {
  161. var trueTop = ((ceiling - (offset + reference) < offset) ?
  162. ceiling - (reference + offset) : offset);
  163. var howMany = (trueTop > 1 ? "s" : "");
  164. docObj.writeln('<INPUT TYPE=BUTTON VALUE="Next ' + trueTop +
  165. ' Result' + howMany + '" ' +
  166. 'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
  167. (reference + offset) + ', ' + offset + ')">');
  168. }
  169. docObj.writeln('</CENTER>');
  170. }
  172. //-->
  173. </SCRIPT>
  174. </HEAD>
  177. <TR>
  179. <FONT FACE="Arial">
  180. <B>Client-Side Search Engine</B>
  181. </TD>
  184. <FORM NAME="search"
  185. onsubmit="validate(document.forms[0].query.value); return false;">
  186. <INPUT TYPE=TEXT NAME="query" SIZE="33">
  187. <INPUT TYPE=HIDDEN NAME="standin" VALUE="">
  188. </FORM>
  189. </TD>
  192. <FONT FACE="Arial">
  193. <B><A HREF="main.html" TARGET="main">Help</A></B>
  194. </TD>
  195. </TR>
  196. </TABLE>
  197. </BODY>
  198. </HTML>

That's a lot of code. The easiest way to understand what's going on here is simply to start at the top, and work down. Fortunately, the code was written to proceed from function to function in more or less the same order.

We'll examine this in the following order:


The first item worth examining is the JavaScript source file records.js. You'll find it in the <SCRIPT> tag at line 5.

It contains a fairly lengthy array of elements called profiles. The contents of this file have been omitted from this book, as they would have to be scrunched together. So after you've extracted the files in the zip file, start up your text editor and open ch01/records.js. Behold: it's your database. Each element is a three-part string. Here's one example:

"|HotSyte- The JavaScript Resource|The " + 
  "HotSyte home page featuring links, tutorials, free scripts, and more"

Record parts are separated by the pipe character (|). These characters will come in handy when matching database records are printed to the screen. The second record part is the document title (it has nothing to do with TITLE tags); the third is the document description; and the first is the document's URL.

By the way, there's no law against using character(s) other than "|" to separate your record parts. Just be sure it's something the user isn't likely to enter as part of a query string (perhaps &^ or ~[%). Keep the backslash character (\) out of the mix. JavaScript will interpret that as an escape character and give you funky search results or choke the app altogether.

Why is all this material included in a JavaScript source file? Two reasons: modularity and cleanliness. If your site has more than a few hundred web pages, you'll probably want to have a server-side program generate the code containing all the records. It's a bit more organized to have this generated in a JavaScript source file.

You can also use this database in other search applications simply by including records.js in your code. In addition, I'd hate to have all that code copied into an HTML file and displayed as source code.

JavaScript Technique:
Using Delimited Strings to Contain Multiple Records

This application relies on searching pieces of information, much like a database. To emulate searching a database, JavaScript can parse (search) an array with similarly formatted data.

It might seem like common sense to set each array element equal to one piece of data (such as a URL or the title of a web page). That works, but you're setting yourself up for potential grief.

You can significantly reduce the number of global array elements if you concatenate multiple substrings with a known delimiter (such as |) into one array element. When you parse each array element, JavaScript's split() method of the String object can create an array of each of the elements. In other words, why have a global array such as:

var records = new Array("The Good", "The Bad",

"and The JavaScript Programmer"),

when you can have a local array inside the function? For example:

var records = "The Good|TheBad|and The JavaScript Programmer".split('|');

Now you're probably thinking, "Six of one and a half dozen of the other. What's the difference?" The difference is that the first version declares three global elements that take up memory until you get rid of them. The second declares only one global element. The three elements created with split('|') at search time are temporary because they are created locally.

With the latter, JavaScript disposes of the records variable after the search function runs. That frees memory. Plus that's less coding for you. For myself, I'll take the second option. We'll hit this concept again when we take a look at the code that does the parsing.

The Global Variables

Lines 9 through 16 of Example 1-1 declare and initialize the global variables.

var SEARCHANY   = 1;
var SEARCHALL   = 2;
var SEARCHURL   = 4;
var searchType   = '';
var showMatches  = 10;
var currentMatch  = 0;
var copyArray  = new Array();
var docObj   = parent.frames[1].document;

The following list explains the variable functions:

Indicates to search using any of the entered terms.
Indicates to search using all of the entered terms.
Indicates to search the URL only (using any of the entered terms).
Indicates the type of search (set to SEARCHANY, SEARCHALL, or SEARCHURL).
Determines the number of records to display per results page.
Determines which record will first be printed on the current results page.
Copy of the temporary array of matches used to display the next or previous set of results.
Variable referring to the document object of the second frame. This isn't critical to the application, but it helps manage your code because you'll need to access the object (parent.frames[1].document) many times when you print the search results. docObj refers to that object, reducing the amount of code and serving as a centralized point for making changes.

The Functions

Next, let's look at the major functions:

validate( )

When the user hits the Enter button, the validate() function at line 18 determines what the user wants to search and how to search it. Recall the three options:

validate() determines what and how to search by evaluating the first few characters of the string it receives. How is the search method set? Using the searchType variable. If the user wants all terms to be included, then searchType is set to SEARCHALL. If the user wants to search the title and description, validate() sets all to false (that's the default, by the way). If the user wants to search the URL, searchType is set to null. Here's how it happens:

Line 19 shows the charAt() method of the String object looking for the + sign as the first character. If found, the search method is set to option 2 (the Boolean AND method).

  if (entry.charAt(0) == "+") {
   entry = entry.substring(1,entry.length);
   searchType = SEARCHALL;

Line 23 shows the substring() method of the String object looking for "url:". If the string is found, searchType is set accordingly.

  if (entry.substring(0,4) == "url:") {
   entry = entry.substring(5,entry.length);
   searchType = SEARCHURL;

What about the substring() methods in lines 20 and 24? Well, after validate() knows what and how to search, those character indicators (+ and url:) are no longer needed. Therefore, validate() removes the required number of characters from the front of the string and moves on.

If neither + nor url: is found at the front of the string, validate() sets variable searchType to SEARCHANY, and does a little cleanup before calling convertString(). The while statements at lines 28 and 32 trim excess white space from the beginning and end of the string.

After discovering the user preference and trimming excess whitespace, validate() has to make sure that there is something left to use in a search. Line 36 verifies that the query string has at least three characters. Searching fewer might not produce useful results, but you can change this to your liking:

  if (entry.length < 3) {
   alert("You cannot search strings that small. Elaborate a little.");

If all goes well to this point, validate() makes the call to convertString(), passing a clean copy of the query string (entry).

convertString( )

convertString() performs two related operations: it splits the string into array elements, and calls the appropriate search function. The split() method of the String object divides the user-entered string by whitespace and puts the outcome into the array searchArray. This happens at line 45 as shown below:

var searchArray = reentry.split(" ");

For example, if the user enters the string "client-side JavaScript development" in the search field, searchArray will contain the values client-side, JavaScript, and development for elements 0, 1, and 2, respectively. With that taken care of, convertString() calls the appropriate search function according to the value of all. You can see this in lines 46 and 47:

if (searchType == (SEARCHALL)) { requireAll(searchArray); }
else { allowAny(searchArray); }

As you can see, one of two functions is called. Both behave similarly, but they have their differences. Here's a look at both functions: allowAny() and requireAll().

allowAny( )

As the name implies, this function gets called from the bench when the application has only a one-match minimum. Here's what you'll see in lines 50-68:

function allowAny(t) {
  var findings = new Array(0);
  for (i = 0; i < profiles.length; i++) {
    var compareElement  = profiles[i].toUpperCase();  
    if(searchType == SEARCHANY) { 
      var refineElement  = 
    else { 
      var refineElement =  
    for (j = 0; j < t.length; j++) {
      var compareString = t[j].toUpperCase();
      if (refineElement.indexOf(compareString) != -1) {
        findings[findings.length] = profiles[i];

The guts behind all three search functions is comparing strings with nested for loops. See the "JavaScript Technique: Nested for Loops" sidebar for more information. The for loops go to work at lines 52 and 63. The first for loop has the task of iterating through each of the profiles array elements (from the source file). For each profiles element, the second for loop iterates through each of the query terms passed to it from convertString().

To ensure that users don't miss matching records because they use uppercase or lowercase letters, lines 53 and 64 declare local variables compareElement and compareString, respectively, and then initialize each to an uppercase version of the record and query term. Now it doesn't matter if users search for "JavaScript," "javascript," or even "jAvasCRIpt."

allowAny() still needs to determine whether to search by document title and description or by URL. So local variable refineElement, the substring that will be compared to each of the query terms, is set according to the value of searchType at line 55 or 59. If searchType equals SEARCHANY, refineElement is set to the substring containing the record's document title and description. Otherwise searchType must be SEARCHURL, so refineElement is set to the substring containing the document URL.

Remember the | symbols? That's how JavaScript can distinguish the different record parts. So the substring() method returns a string starting from 0 and ending at the character before the first instance of "|HTTP", or returns a string starting at the first instance of "|HTTP" until the end of the element. Now we have what we're about to compare with what the user entered. Check it out at line 65:

if (refineElement.indexOf(compareString) != -1) {      
  findings[findings.length] = profiles[i];  

If compareString is found within refineElement, we have a match (it's about time). That original record (not the URL-truncated version we searched) is added to the findings array at line 66. We can use findings.length as an indexer to continually assign elements.

Once we've found a match, there is certainly no reason to compare the record with other query strings. Line 67 contains the break statement that stops the for loop comparison for the current record. This isn't strictly necessary, but it reduces excess processing.

After iterating through all records and search terms, allowAny() passes any matching records in the findings array to function verifyManage() at lines 95 through 101. If the search was successful, function formatResults() gets the call to print the results. Otherwise, function noMatch() will let the user know that the search was unsuccessful. Functions formatResults() and noMatch() are discussed later in the chapter. Let's finish examining the remaining search methods with requireAll().

requireAll( )

Put a + in front of your search terms, and requireAll()gets the call. This function is nearly identical to allowAny(), except that all terms the user enters must match the search. With allowAny(), records were added to the result set as soon as one term matched. In this function, we have to wait until all terms have been compared to each record before deciding to add anything to the result set. Line 74 starts things off:

function requireAll(t) {
  var findings = new Array();
  for (i = 0; i < profiles.length; i++) {  
    var allConfirmation = true;    
    var allString       = profiles[i].toUpperCase();
    var refineAllString = allString.substring(0,  
    for (j = 0; j < t.length; j++) { 
      var allElement = t[j].toUpperCase();
      if (refineAllString.indexOf(allElement) == -1) { 
        allConfirmation = false;
    if (allConfirmation) {
      findings[findings.length] = profiles[i];

JavaScript Technique: Nested for Loops

Both the searching functions allowAny() and requireAll() use nested for loops. This is a handy technique to iterate multidimensional arrays as opposed to single-dimension arrays. (JavaScript arrays are technically one-dimensional. However, JavaScript can emulate multidimensional arrays as described here.) Consider this five-element, single-dimension array:

var numbers = ("one", "two", "three", "four", "five");

If you want to compare a string to each of these, you simply run a for (or while) loop, comparing each array element to the string as you go. Like this:

for (var i = 0; i < numbers.length; i++) {

if (myString == numbers[i]) { alert("That's the number");



Not too demanding, so let's up the ante. Multidimensional arrays are, well, arrays of arrays. For example:

var numbers = new Array(

new Array("one", "two", "three", "four", "five"),

new Array("uno", "dos", "tres", "cuatro", "cinco"),

new Array("won", "too", "tree", "for", "fife")


A single for loop won't cut it. We'll need more fire power. The first numbers array is a single-dimension array (1 × 5). The new version is a multidimensional array (3 × 5). Going through all 15 elements (3 × 5) means we'll need an extra loop:

for (var i = 0; i < numbers.length; i++) { // 1...

for (var j = 0; j < numbers[i].length; j++) { // and 2.

if (myString == numbers[i][j]) {

alert("Finally found it.");





That's the two-dimensional answer to getting a shot at each element. Let's take it a notch further. What if we build a color palette in a table of all 216 web- safe colors--one in each cell? Nested for loops to the rescue. This time, however, we'll only use a single dimension array.

Using hexadecimal numbers, web-safe colors come in six-digit groups--two digits for each color component--such as FFFFF, 336699, and 99AACC. The two-digit pairs that make up all web-safe colors are: 33, 66, 99, AA, CC, and FF. Let's spark up an array:

var hexPairs = new Array("33","66","99","AA","CC","FF");


"There's only one array and one dimension. I want my money back."

Don't run to the bookstore yet. There are three dimensions, but we'll use the same array for each dimension. Here's how:

var str = ' ';


// Strike up a table

document.writeln('<H2>Web Safe Colors</H2>' +


for (var i = 0; i < hexPairs.length; i++) {

// Create a row


for (var j = 0; j < hexPairs.length; j++) {

for (var k = 0; k < hexPairs.length; k++) {

// Create a string of data cells for the row with whitespace in each

// Notice each background color is made with three hexPairs elements

str += '<TD BGCOLOR="' + hexPairs[i] + hexPairs[j] + hexPairs[k] +



// Write the row of data cells and reset str


str ='';


// End the row



// End the table


Drop this code in a web document (it's in the zip file, at \Ch01\websafe.html ), and you'll get a 6 × 36 table with all 216 (that's 6 × 6 × 6) web-safe colors. Three for loops and three dimensions. Of course, you could modify the palette table in plenty of ways, but this just shows you how nested for loops can solve your coding woes.

At first glance, things seem much as they were with allowAny(). The nested for loops, the uppercase conversion, and the confirmation variable--they're all there. Things change, however, at lines 79-80:

var refineAllString = allString.substring(0,allString.indexOf('|HTTP'));

Notice that variable searchType was not checked to determine which part of the record to keep for searching as it was in allowAny() at line 50. There's no need. requireAll() gets called only if searchType equals SEARCHALL (see line 46). URL searching doesn't include the Boolean AND method, so it's a known fact that the document title and description will be compared.

Function requireAll() is a little tougher to please. Since all the terms a user enters must be found in the compared string, so the searching logic will be more restrictive than it is in allowAny(). See lines 83 through 86:

if (refineAllString.indexOf(allElement) == -1) { 
  allConfirmation = false;

It will be far easier to reject a record the first time it doesn't match a term than it will be to compare the number of terms with the number of matches. Therefore, the first time a record does not contain a match, the continue statement tells JavaScript to forget about it and move to the next record.

If all terms have been compared to a record and local variable allConfirmation is still true, we have a match. allConfirmation becomes false the moment a record fails to match its first term. The current record is then added to the temporary findings array at line 89. This condition is harder to achieve, but the search results will likely be more specific.

Once all records have been evaluated this way, findings is passed to verifyManage() to check for worthy results. If there are any matches at all, formatResults() gets the call. Otherwise, requireAll() calls noMatch() to bring the bad news to the user.

verifyManage( )

As you've probably realized, this function determines whether the user's search produced any record matches and calls one of two printout functions pending the result. It all starts at line 95:

function verifyManage(resultSet) {
if (resultSet.length == 0) { 
    copyArray = resultSet.sort();
    formatResults(copyArray, currentMatch, showMatches);    

Both allowAny() and requireAll() call verifyManage() after running the respective course and pass the findings array as an argument. Line 96 shows that verifyManage() calls function noMatch() if array resultSet (a copy of findings) contains nothing.

If resultSet contains at least one matched record, however, global variable copyArray is set to the lexically sorted version of all the elements in resultSet. Sorting is not necessary, but it's a great way to add order to your result set, and you don't have to worry about the order in which you add records to the profiles array. You can keep adding them on the end, knowing that they'll be sorted if a match occurs.

So why should we make an extra copy of a bunch of records we already have? Remember that findings is a local, and thus temporary, array. Once a search has been performed (that is, the application executes one of the search functions), findings dies, and its allocated memory is freed for further use. That's a good thing. There's no reason to hold onto memory we could possibly use elsewhere, but we still need access to those records.

Since the application displays, say, 10 records per page, users potentially see only a subset of the matching results. Variable copyArray is global, so sorting the temporary result set and assigning that to copyArray keeps all matching records intact. Users can now view the results 10, 15, or however many at a time. This global variable will keep the matching results until the user submits a new query.

The last thing verifyManage() does is call formatResults(), passing an index number (currentMatch), indicating which record to begin with and how many records to display per page (showMatches). Both currentMatch and showMatches are global variables. They don't die after functions execute. We need them for the life of the application.

noMatch( )

noMatch() does what it implies. If your query produces no matches, this function is the bearer of the bad news. It is rather short and sweet, though it still generates a custom results (or lack of results) page, stating that the query term(s) the user entered didn't produce at least one match. Here it is starting at line 103:

function noMatch() {;
  docObj.writeln('<HTML><HEAD><TITLE>Search Results</TITLE></HEAD>' + 
    '<FONT FACE=Arial><B><DL>' + 
    '<HR NOSHADE WIDTH=100%>"' + document.forms[0].query.value + 
    '" returned no results.<HR NOSHADE WIDTH=100%>' + 

formatResults( )

This function's job is to neatly display the matching records for the user. Not terribly difficult, but this function does cover a lot of ground. Here are the ingredients for a successful results display:

The HTML head and title

The HTML head and title are straightforward. Lines 116 through 129 print the head, title, and the beginning of the body contents. Take a look:

function formatResults(results, reference, offset) {
  var currentRecord = (results.length < reference + offset ?  
    results.length : reference + offset);;
  docObj.writeln('<HTML><HEAD><TITLE>Search Results</TITLE>\n</HEAD>' + 
    '<FONT FACE=Arial><B>Search Query: <I>' + 
    parent.frames[0].document.forms[0].query.value + '</I><BR>\n' + 
    'Search Results: <I>' + (reference + 1) + ' - ' + currentRecord + 
    ' of ' + results.length + '</I><BR><BR></FONT>' + 
    '<FONT FACE=Arial SIZE=-1><B>' + 
    '\n\n<!- Begin result set //-->\n\n\t<DL>');

Before printing the heading and title, let's find out which record we're going to start with. We know the first record to print starts at results[reference]. And we should display offset records unless reference + offset is greater than the total number of records. To find out, the ternary operator is again used to determine which is larger. Variable currentRecord is set to that number at line 117. We'll use that value shortly.

Now, formatResults() prints your run-of-the-Internet HTML heading and title. The body starts with a centered table and a horizontal rule. The application easily gives the user a reminder of the search query (line 125), which came from the form field value:


Things get more involved at line 126, however. This marks the beginning of the result set. The line of printed text on the page displays the current subset of matching records and the total number of matches, for instance:

Search Results:  1 - 10 of 38

We'll need three numbers to pull this off--the first record of the subset to display, the number of records to display, and the length of copyArray, where the matching records are stored. Let's take a look at this in terms of steps. Remember, this is not the logic used to display the records. This logic lets the user know how many records and with which record to start. Here is how things happen:

  1. Assign the number of the current record to variable reference, then print it.
  2. Add another number called offset, which is how many records to display per page (in this case, 10).
  3. If the sum of reference + offset is greater than the total number of matches, print the total number of matches. Otherwise, print the sum of reference + offset. (This value has already been determined and is reflected in currentRecord).
  4. Print the total number of matches.

Steps 1 and 2 seem simple enough. Recall the code in verifyManage(), particularly line 99:

formatResult(copyArray, currentMatch, showMatches);

The local variable results is a copy of copyArray. The variable reference is set to currentMatch, so the sum of reference + offset is the sum of currentMatch + showResults. In the first few lines of this code (13 and 14 to be exact), showMatches was set to 10, and currentMatch was set to 0. Therefore, reference starts as 0, and reference + offset equals 10. Step 1 is taken care of as soon as reference is printed. The math we just did takes care of step 2.

In step 3, we use the ternary operator (at lines 117-118) to decide whether the sum of reference + offset is greater than the total number of matches. In other words, will adding offset more records to reference yield a number higher than the total number of records? If reference is 20, and there are 38 total records, adding 10 to reference gives us 30. The display would look like this:

Search Results: 20 - 30 of 38

If reference is 30, however, and there are 38 total records, adding 10 to reference gives us 40. The display would look like this:

Search Results: 30 - 40 of 38

Can't happen. The search engine cannot display records 39 and 40 if it only found 38. This then indicates that the end of the records has been reached. So the total number of records will be displayed instead of the sum of reference + offset. That brings us to step 4, and the end of the process:

Search Results: 30 - 38 of 38

TIP: Function formatResults() is sprinkled with special characters such as \n and \t. \n represents a newline character, equivalent to pressing Enter on your keyboard while writing code in your text editor. \t is equivalent to pressing the Tab key. All that these characters do in this case is make the HTML of the search results look neater if you view the source code. I included them here to show you how they look. Keep in mind that they are not necessary and don't affect your applications. If you think they clutter your code, don't use them. I use them sparingly in the rest of the book.

Displaying document titles, descriptions, and linked URLs

Now that the subset of records has been indicated, it's time to print that subset to the page. Enter lines 130 through 143:

if (searchType == SEARCHURL) {
  for (var i = reference; i < currentRecord; i++) {
    var divide = results[i].split('|'); 
    docObj.writeln('\t<DT>' + '<A HREF="' + divide[2] + '">' + 
      divide[2] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>\n\n');
else {
  for (var i = reference; i < currentRecord; i++) {
    var divide = results[i].split('|');   
    docObj.writeln('\n\n\t<DT>' + '<A HREF="' + divide[2] + '">' + 
      divide[0] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>');

Lines 131 and 138 show both for loops, which perform the same operation with currentRecord, except that the order of the printed items is different. Variable searchType comes up again. If it equals SEARCHURL, the URL will be displayed as the link text. Otherwise, searchType equals SEARCHANY or SEARCHALL. In either case the document title will be displayed as the link text.

The type of search has been determined, but how do you neatly display the records? We need only loop through the record subset, and split the record parts accordingly by title, description and URL, placing them however we so desire along the way. Here is the for loop used in either case (URL search or not):

for (var i = reference; i < lastRecord; i++) {

Now for the record parts. Think back to the records.js file. Each element of profiles is a string that identifies the record "|" separating its parts. And that is how we'll pull them apart:

var divide = results[i].split('|');

For each element, local variable divide is set to an array of elements also separated by "|". The first element (divide[0]) is the URL, the second element (divide[1]) is the document title, and the third (divide[2]) is the document description. Each of these elements is printed to the page with accompanying HTML to suit (I chose <DL>, <DT>, and <DD> tags). If the user searched by URL, the URL would be shown as the link text. Otherwise, the document title becomes the link text.

Adding "Previous" and "Next" buttons

The only thing left to do is add buttons so that the user can view the previous or next subset(s) of records. This actually happens in function prevNextResults(), which we'll discuss shortly, but here are the last few lines of formatResults():

  docObj.writeln('\n\t</DL>\n\n<!- End result set //-->\n\n');
  prevNextResults(results.length, reference, offset);      
  docObj.writeln('<HR NOSHADE WIDTH=100%>' + 

This part of the function calls prevNextResults(), adds some final HTML, then sets the focus to the query string text field. There isn't much to it, but we'll discuss it shortly.

prevNextResults( )

If you've made it this far without screaming, this function shouldn't be that much of a stretch. prevNextResults() is as follows, starting with line 152.

function prevNextResults(ceiling, reference, offset) {
  if(reference > 0) {
    docObj.writeln('<INPUT TYPE=BUTTON VALUE="Prev ' + offset + 
      ' Results" onClick="' + 
      parent.frames[0].formatResults(parent.frames[0].copyArray, ' + 
      (reference - offset) + ', ' + offset + ')">');
  if(reference >= 0 && reference + offset < ceiling) {
    var trueTop = ((ceiling - (offset + reference) < offset) ?  
      ceiling - (reference + offset) : offset);
    var howMany = (trueTop > 1 ? "s" : "");
    docObj.writeln('<INPUT TYPE=BUTTON VALUE="Next ' + trueTop +  
      ' Result' + howMany  + '" onClick="' + 
      parent.frames[0].formatResults(parent.frames[0].copyArray, ' + 
      (reference + offset) + ', ' + offset + ')">');

JavaScript Technique: Go Easy on document.write( )

Take another look at formatResults(). You'll see that HTML written to the page with a call to document.write() or document.writeln(). The string passed to these methods is generally long and spans multiple lines concatenated by +. While you may argue that the code would be more readable with a call to document.writeln() on each line, there is a reason for doing otherwise. Here's what I mean. The few lines of formatResults() are as follows:

function formatResults(results, reference, offset) {;

docObj.writeln('<HTML>\n<HEAD>\n<TITLE>Search Results</TITLE>\n</HEAD>' +




'<FONT FACE=Arial><B>Search Query: <I>' +

parent.frames[0].document.forms[0].query.value + '</I><BR>\n' +

'Search Results: <I>' + (reference + 1) + ' - ' +

(reference + offset > results.length ? results.length :

reference + offset) +

' of ' + results.length + '</I><BR><BR></FONT>' +

'<FONT FACE=Arial SIZE=-1><B>' +

'\n\n<!- Begin result set //-->\n\n\t<DL>');

There is only one method call to write the text to the page. Not too attractive. One alternative would be to line things up neatly with a method call on each line:

function formatResults(results, reference, offset) {;

docObj.writeln('<HTML><HEAD><TITLE>Search Results</TITLE>\n</HEAD>');


docObj.writeln('<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER ' +


docObj.writeln('<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP ');

docObj.writeln('<FONT FACE=Arial><B>' + 'Search Query: <I>' +

parent.frames[0].document.forms[0].query.value + '</I><BR>\n');

docObj.writeln('Search Results: <I>' + (reference + 1) + ' - ' );

docObj.writeln( (reference + offset > results.length ?

results.length : reference + offset) +

' of ' + results.length + '</I><BR><BR></FONT>' +

'<FONT FACE=Arial SIZE=-1><B>');

docObj.writeln('\n\n<!- Begin result set //-->\n\n\t<DL>');

That might look more organized, but each of those method calls means a little more work for the JavaScript engine. Think about it. What would you rather do: make five trips to and from the store and buy things a little at a time, or go to the store once and buy it all the first time? Just pass a lengthy text string separated with "+" signs, and be done with it.

This function prints a centered HTML form at the bottom of the results page with one or two buttons. Figure 1-3 shows a results page with both a "Prev" and a "Next" button. There are three possible combinations of buttons:

Three combinations. Two buttons. That means this application must know when to print or not print a button. The following list describes the circumstances under which each combination will occur.

"Next" Button Only
Where should we include a Next button? Answer: every results page except the last. In other words, whenever the last record (reference + offset) of the results page is less than the total number of records.
Now, where do we exclude the "Prev" button? Answer: on the first results page. In other words, when reference equals 0 (which we got from currentMatch).
"Prev" and the "Next" Buttons
When should both be displayed? Given that a "Next" button should be included on every results page except the last, and a "Prev" button should be included on every results page except the first, we'll need a "Prev" button as long as reference is greater than 0, and a "Next" button if reference + offset is less than the total number of records.
"Prev" Button Only
Knowing when to include a "Prev" button, under what circumstances should we exclude the "Next" button? Answer: when the last results page is displayed. In other words, when reference + offset is greater than or equal to the total number of matching records.

Things might still be a little sketchy, but at least we know when to include which button(s), and the if statements in lines 154 and 160 do just that. These statements include one or both the "Prev" and "Next" buttons depending on the current subset and how many results remain.

Both buttons call function formatResults() when the user clicks them. The only difference is the arguments that they pass, representing different result subsets. Both buttons are similar under the hood. They look different because of the VALUE attribute. Here is the beginning of the "Prev" button at lines 155-156:

docObj.writeln('<INPUT TYPE=BUTTON VALUE="Prev ' + offset + ' Results" ' + 

Now the "Next" button at lines 164-165:

docObj.writeln('<INPUT TYPE=BUTTON VALUE="Next ' + trueTop + ' Result' + 

Both lines contain the TYPE and VALUE attributes of the form button plus a number indicating how many previous or next results. Since the number of previous results is always the same (offset), the "Prev" button value displays that number, for example, "Prev 10 Results." The number of next results can vary, however. It is either offset or the number remaining if the final subset is less than offset. To address that, variable trueTop is set to that value, whichever it is.

Notice how the value of the "Prev" button always contains the word "Results." This makes sense. The showMatches never changes throughout the app. In this case it is and always will be 10. So the user can always count on seeing 10 previous results. However, that isn't always the case for the amount of "Next" results. Suppose the last subset contains only one record. The user shouldn't see a button labeled "Next 1 Results." That's incorrect grammar. To clean this up, prevNextResults() contains a local variable named howMany that uses the ternary operator once again. You'll find it at line 163:

var howMany = (trueTop > 1 ? "s" : "");

If trueTop is greater than 1, howMany is set to the string "s". If trueTop equals 1, howMany is set to the empty string "". As you can see at line 165, howMany is printed immediately after the word "Result." If there is only one record in the subset, the word "Result" appears unchanged. If there are more, however, the user sees "Results."

The final step in both buttons is "telling" them what to do when they are clicked. I mentioned earlier that the onClick events of both buttons call formatResults(). Lines 157-158 and 166-167 dynamically write the call to formatResults() in the onClick event handler of either button. Here is the first set (the latter half of the document.writeln() call):

'onClick="' + parent.frames[0].formatResults(parent.frames[0].copyArray, ' + 
 (reference - offset) + ', ' + offset + ')">');

The arguments are determined with the aid of the ternary operator and written on the fly. Notice the three arguments passed (once the JavaScript generates the code) are copyArray, reference - offset, and offset. The "Prev" button will always get these three arguments. By the way, notice how formatResults() and copyArray are written:




That may seem strange at first, but remember that the call to formatResults() does not happen from nav.html (parent.frames[0]). It happens from the results frame parent.frames[1], which has no function named formatResults() and no variable named copyArray. Therefore, functions and variables need this reference.

The "Next" button gets a similar call in the onClick event handler, but wait a sec. Don't we have to deal with the possibility of less than offset results in the last results subset of copyArray just as we did in formatResults() when displaying the range of currently viewed results? Nope. Function formatResults() takes care of that decision process; all we do is add reference to offset and pass it in. Take a look at lines 166-167, again the latter half of the document.writeln() method call:

'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' + 
(reference + offset) + ', ' + offset + ')">');

JavaScript Technique: The Ternary Operator

After that section, you must have seen this one coming. The ternary operator is pretty helpful, so here's my sermon. Ternary operators require three operands, and they are used throughout this app as a one-line if-else statement. Here's the syntax straight from Netscape's JavaScript Guide for Communicator 4.0, Chapter 9:

(condition) ? val1 : val2

This conditional operator, when properly populated, acts upon val1 if condition evaluates to true, and val2 otherwise. I'm making all the fuss about it because in many cases I find it makes code easier to read and there is usually less to write. This operator can be especially helpful if you're coding within several nested statements.

The ternary operator is not the cure for everything. If you have multiple things that need to happen if condition is true or false, take the if-else route. Otherwise, give this a try in your code.


nav.html has very little static HTML. Here it is again, starting with line 174:

  <FONT FACE="Arial">
  <B>Client-Side Search Engine</B>
  <FORM NAME="search" 
    onsubmit="validate(document.forms[0].query.value); return false;">
  <INPUT TYPE=TEXT NAME="query" SIZE="33">
  <FONT FACE="Arial">
  <B><A HREF="main.html" TARGET="main">Help</A></B>

There aren't really any surprises. You have a form embedded in a table. "Submitting" the form executes the code we've been covering. The only question you might have is: "How can the form be submitted without a button?" As of the HTML 2.0 specification, most browsers (including Navigator and MSIE) have enabled form submission with a single text field form.

There's no law saying you have to do it this way. Feel free to add a button or image to jazz it up.

Building Your Own JavaScript Database

Eventually you'll want to replace the records I've provided with your own records. You can do this in three easy steps.

  1. Open records.js in your text editor.
  2. Remove the records already there so that the file looks like this:
  3. var profiles = new Array( );
  4. For each record you want to add, use the following syntax:
  5. "Your_Page_Title|Your_Page_Description|http://your_page_url/file_name.html",

Add as many of these elements between the parentheses as you want. Be sure to include the comma at the end of each recordexcept the last one. Notice also the page title, description, and URL are each separated by "|" (the pipe character). Don't use any of those in your titles, descriptions, or URLs. That'll cause JavaScript errors. Remember, too that if you include double quotes (") other than the ones on the outside, be sure to escape them with a backslash (e.g., use \" instead of just ").

Potential Extensions

The search engine is pretty useful the way it is. What's even better is that you can make some significant improvements or changes. Here are some possibilities:

JavaScript 1.0 Compatibility

You know it, and I know it. Both of the major browsers are in the latter 4.x or early 5.x versions. Both are free. But there are still people out there clunking along with MSIE 3.02 or NN 2.x. I still get a surprising hit count of visitors with those credentials to HotSyte--The JavaScript Resource (

Since a search engine is pretty much a core feature of a web site, you might consider converting this app for JavaScript 1.0. Fortunately, all you have to do is go through the code listed earlier, line by line, figure out which features aren't supported in JavaScript 1.0, and change all of them.

OK. I already did that, but admit it: I had you going. Actually, you'll find the modified version in /ch01/js1.0/. Open index.html in your browser just like you did with the original. In this section, we'll take a quick look at what will make the app work in JavaScript 1.0 browsers. There are three changes:

NN 2.x and MSIE 3.x do not support .js source files.[1] The workaround for this is to embed the profiles array in nav.html. The second change eliminates the call to resultSet.sort() in line 90. That means your results will not be sorted in dictionary order, but by the way you have them chronologically listed in profiles. The last change is eliminating the split() method. JavaScript 1.0 does not support that either; the workaround takes care of that, but it degrades performance.


That's what my economics professor wrote on the chalkboard my freshman year at Florida State University. The translated acronym: Thar' Ain't No Such Thang As A Free Lunch. In other words, these changes give you older browser version compatibility, but cost you in functionality and code management.

Without support for .js files, you have to dump that profiles array into your nav.html. That will be quite unsightly and more unmanageable if you want to include those records in other searches.

The sort() method, while not critical to the operation, is a great feature. People might have to view all subsets of matched records because the records are in no particular order. Of course, you could place the results in the array alphabetically, but that's no picnic either. Or you write your own sort method for JavaScript 1.0. The split() method is arguably the least of your troubles. The JavaScript 1.0 version of the app has a workaround, so it really isn't an issue.

Make It Harder to Break

As it stands, you can pass the pipe character as part of the search query. Why not add the functionality to remove any characters from the query used as the string delimiters? That makes the app harder to break.

Display Banner Ads

If your site is gets a lot of traffic, why not use it to make some extra money?

How? Try this. Suppose you want to randomly display five banner ads (no particular order in this case). If you have several ad image URLs in an array, you could pick one to load at random. Here's the array.

var adImages = new Array("pcAd.gif", "modemAd.gif", "webDevAd.gif");

Then you might randomly display one on the results page like so:

document.writeln('<IMG SRC=' + ads[Math.floor(Math.random(ads.length))] +

Add Refined Search Capabilities

You can have some great programming fun with this concept. For example, suppose the user could select from array elements to search. Then the user could narrow seach results accordingly.

Consider displaying a set of checkboxes under the text field in nav.html. Maybe like this:

<INPUT TYPE=CHECKBOX NAME="group" VALUE="97"> 1997 Records <BR>
<INPUT TYPE=CHECKBOX NAME="group" VALUE="98"> 1998 Records <BR>
<INPUT TYPE=CHECKBOX NAME="group" VALUE="99"> 1999 Records <BR>

Use this checkbox group to determine which arrays to search, in this case profiles97, profiles98, or profiles99.

There are many things you can add to increase the user's ability to refine searches. One easy one is to offer case-sensitive and case-insensitive queries. As it stands now, case does not matter, but you can change that by adding a checkbox allowing either style.

You could also expand search refinement by broadening Boolean searches from the current AND and OR searches to AND, OR, NOT, ONLY, even LIKE. Here is a breakdown of the general meanings:

Record must contain both terms on the left and right of AND.
Record can contain either of the terms on the left and right of OR.
Record must not contain the term(s) to the right of NOT.
Record must contain this and only this record.
Record can contain term(s) spelled like or sounding like.

This takes some work (especially LIKE), but users would be quite amazed at your wizardry.

Cluster Sets

Another popular and useful technique is to establish cluster sets. Cluster sets are predefined word groups that automatically return predefined results. For example, if a user includes the term "mutual funds" anywhere in the query string, you could automatically generate results containing records featuring your company's financial products. This technique takes a bit more planning, but it would be a great feature in a search application.

1. Actually that's a stretch. Some versions of MSIE 3.02 do support JavaScript source files.

Back to: JavaScript Cookbook Home | O'Reilly Bookstores | How to Order | O'Reilly Contacts
International | About O'Reilly | Affiliated Companies | Privacy Policy

© 2001, O'Reilly & Associates, Inc.