Day 13

Searching by Keywords

by Paul Mahar


CONTENTS

Today's JavaScript journey is somewhat reminiscent of yesterday's. The objective is to create a form that lets shoppers search for books in much the same way you can search for Web sites using the Yahoo! and Excite search engines. As with the Cart form from Day 12, the Keyword form will evolve through three stages of functionality:

Creating the Keyword form doesn't take as long as creating the Cart form. At the end of the day, there will be some time left to prepare for Day 14, the final day of the shopping cart development cycle.

TIP
All three versions of the Keyword form are available on the CD-ROM with the Day 13 source files as keyword1.jfm, keyword2.jfm, and keyword3.jfm. You can use these files as a starting point if you want to skip over some of the exercises or as a reference point if you get lost along the way.

The Simple Search

All the controls in the Keyword form work through the Form Constructor. You can lay out every component with the Form Designer. So far, the controls used in any of the forms have been only HTML, text, image, button, and reset. This form also includes a checkbox and a set of radio buttons. Along with the inherited toolbar buttons, the Keyword form contains the following:

Whenever you need to display a limited set of choices, you can use a select list, radio buttons, or a checkbox. The control you use corresponds to the number of choices in your list. If you have more than five choices, such as the product names in the Quick form, a select list is the way to go. Radio buttons are easier to use than a select list, but require more form real estate. Radio buttons are appropriate where there are less than five choices and your form has room. A checkbox is almost always the best choice when you have only two choices and one excludes the other.

There are a few places where radio buttons are preferable. In the Keyword form you could substitute two radio buttons for a checkbox labeled Match all keywords. A benefit of using two radio buttons instead of a checkbox is that the labels can be more descriptive. Consider having radio buttons labeled Require a match on all keywords and Find books with any keyword instead of the Match all keywords checkbox. Try not to substitute a checkbox for radio buttons where the unchecked condition is unclear or where it might appear judgmental. For instance, it is not a good idea to use only a checkbox labeled Male in place of radio buttons labeled Male and Female.

The Visual Layout of the Keyword Form

The steps in this section guide you through the user interface design of the Keyword form. There are three phases to the design. In the first set of steps, you create the form and add all necessary controls. You can then test the design by incorporating the Keyword form into the Help form's include file. The second series of steps takes care of cosmetic issues that appear when running the form. The last series of design steps includes adding a query to the form and setting up some JavaScript placeholders. The following is the first series of steps for creating the initial version of the Keyword form:

  1. Open the Form Designer to start work on the new Keyword form. Use the Script Pad to provide a filename and bypass the Expert prompt.
    _sys.forms.design("keyword.jfm") ;
  2. Make sure you are using the toolbar custom form class. If the new form does not contain toolbar buttons, select File|Set Custom Form Class and select ToolbarCForm from toolbar.jcf.
  3. You can create a new htmlHelp control from scratch or get some reusability the old fashioned way, through cut and paste. Open the Quick form in the Form Designer while leaving the Keyword Form Designer window open. Copy the htmlHelp control from the Quick form to the new Keyword form as shown in Figure 13.1.
    Figure 13.1 : Copying the htmlHelp control from the Quick form.


    When you paste a control from the Clipboard, the name property reverts to a default. Change the name from HTML1 back to htmlHelp.

  4. Use the text property tool button to open the Text Property Builder. Change the text property to the following lines:
    To search the book store, type keywords in the box below
    separated by a space, then click the "Search" button.
  5. Add a Text control beneath the help text. Set the properties to the values listed in Table 13.1.

Table 13.1. Property values for textKeywords.

PropertyValue
nametextKeywords
left1
top5.5
width55

  1. Add three radio buttons and set their properties to the values shown in Table 13.2. After adding the radio buttons, your form should resemble the one in Figure 13.2.
    Figure 13.2 : Adding radio buttons to the Keyword form.

Table 13.2. Property values for the Keyword form radio buttons.

namegroupName text
height
left
top
width
radioTitlekeyfieldSearch by Title
1
1
6.5
20
radioAuthorkeyfieldSearch by Author
1
1
7.5
20
radioISBNkeyfieldSearch by ISBN
1
1
8.5
20

TIP
When adding groups of radio buttons, add all the controls for a single group at one time. If you add two radio buttons and follow that by adding an HTML control before adding a third radio button, the Form Designer assumes the third radio button belongs to a new group.

  1. Add a checkbox below the radio buttons. Use the property values listed in Table 13.3.

Table 13.3. Property values for checkAll.

PropertyValue
namecheckAll
height1
left1
top10.25
width20
textMatch all keywords

  1. Add the Search button below the checkbox. Use the property values listed in Table 13.4.

Table 13.4. Property values for buttonSearch.

PropertyValue
namebuttonSearch
left1
top11.5
width12
textSearch

  1. Add the reset control to the left of the Search button. Set the position properties to top 11.5, left 15, and width 12. You can leave the name and text properties as the default values.
  2. Save the form when it resembles Figure 13.3.
    Figure 13.3 : Adding a reset control to the Keyword form.

All the required controls are now on the form. This is a good point to test the form in a browser. Before you can do that, you need to add _sys.script.load() and #include statements to the store.h file and force a recompile of the Help form. The application relies on the Help form to open all other forms. Any time you add or remove a form, you need to update the store.h file and recompile the Help form. You can force the recompile by deleting the help.jfo file or by using _sys.scripts.compile("help.jfm"). Listing 13.1 shows the new lines in bold. See Day 9 for an analysis of the store.h file.


Listing 13.1. Adding the Keyword form to store.h.

 1: #define DEBUG // for development only

 2: #ifndef STORE

 3:  #define STORE

 4:  #ifdef DEBUG

 5:  // Load scripts

 6:  // _sys.script.load statements go here.

 7:  _sys.scripts.load("cart.jfm") ;

 8:  _sys.scripts.load("detail.jfm") ;

 9:  _sys.scripts.load("help.jfm") ;

10:  _sys.scripts.load("keyword.jfm") ;

11:  _sys.scripts.load("message.jfm") ;

12:  _sys.scripts.load("quick.jfm") ;

13:  _sys.scripts.load("results.jfm") ;

14:  #elseif

15:  // Include scripts

16:  return null

17:  // #include statements go here

18:  #include <cart.jfm>

19:   #include <detail.jfm>

20:  #include <help.jfm>

21:  #include <keyword.jfm>

22:   #include <message.jfm>

23:  #include <quick.jfm>

24:  #include <results.jfm>

25:  #endif

26: #endif


When you do run the form in a browser, the Keyword form becomes slightly taller. The browser adds some vertical space around each rule line. Figure 13.4 shows the Keyword form running in Internet Explorer.

Figure 13.4 : The Keyword form in a browser before adding a third rule line.


NOTE
Another way to add vertical spacing that only appears in a browser is to add line break tags. The HTML tag for a line break is <BR>.

There are a few minor cosmetic problems with the current form. The form has no title, the Keyword button is visible, and there is not much separation between the radio buttons and the checkbox. Follow these steps to add one more rule line and take care of the other loose ends:

  1. Open the Keyword form in the Designer.
  2. Inspect the form and change the title property to Boingo's Books - Keyword Search as shown in Figure 13.5.
    Figure 13.5 : Giving the Keyword form a proper title.

  3. Add a new rule line with the positions shown in Table 13.5.
  4. Enter a code block to hide the Keyword toolbar button. Inspect form and enter the following code block in the form's onServerLoad event.
    this.buttonKeyword.visible = false ;

Table 13.5. Position properties for the third rule line.

PropertyValue
namerule3
left0
top10
right70

  1. Save the form when it looks like the one in Figure 13.6. After saving the form, close the Designer and run the form through a browser.
    Figure 13.6 : Designing the Keyword form with rule lines.

The revised Keyword form looks better in the area of the checkbox, but there is still a slight problem with the Text control. Figure 13.7 shows how the form opens with a default value of Text. It really should be blank.

Figure 13.7 : Running the Keyword form in Netscape Navigator.


NOTE
Forms can appear with different color schemes when using Netscape Navigator. Users can override the colors in the HTML by checking the Always use my colors option in the Navigator Preferences dialog.

You can fix the default text problem and get ready for some JavaScript by creating a new method and a new function. You need to code only the following two routines for the Keyword search:

Before getting into the meat of these functions, create placeholders for both and take care of the other issues. This is the last time you need to use the Form Designer for developing the Keyword form. Here are the steps:

  1. Open the Keyword form in the Form Designer.
  2. Inspect form.textKeywords and blank out the value property.
  3. Drag and drop the Title table from the IntraBuilder Explorer onto the Form Designer. Remove the full path from the sql property of the new query. After removing the path, the remaining SQL command should match the following:
    SELECT * FROM "title.DBF"
  4. Use the Inspector to create a new method linked to the onServerClick event of the Search button. For now, you can leave the method as an empty block.
    function buttonSearch_onServerClick()
    {
    }
  5. Select the Method Editor window. From the drop-down list at the top of the editor, select (General) as shown in Figure 13.8. Enter the following function:
    function KeywordCanGetRow()
    Figure 13.8 : Selecting (General) to add a function outside the class.


    {
    return ( true )
    }

  6. Save your changes, close the Designer, and try the application in a browser.

NOTE
There is no menu choice to generate a function definition outside the class. The Method|New Method option always places the code within the class.

Using the canGetRow Event

The form looks great. Now, let's make it do something. More specifically, let's make it find some rows and call the Results form for display. The Results form has some code already set up to handle keyword searches. There are four properties that the Results form passes on to the Detail form if the current search is a keyword search. To call the Results form correctly, the Keyword form must create all four expected properties. The four properties are as follows:

If you review the buttonDetail_onServerClick() in the Results form, you will find the following if block. This is where the four properties are passed from the Results form to the Detail form. In the Keyword form, similar assignments are made in the buttonSearch_onServerClick() method.


if (! (this.form.title1.rowset.canGetRow == null)) {

   nextForm.scopeAll = this.form.scopeAll ;

   nextForm.keyfield = this.form.keyfield ;

   nextForm.keywords = this.form.keywords ;

   nextForm.title1.rowset.canGetRow = KeywordCanGetRow ;

}

Open the Keyword form in the Script Editor and locate the buttonSearch_onServerClick() method. Because it's the only method, you will find it right after the default rowset assignment. Update the method as shown in Listing 13.2. When you're done, try running the application.


Listing 13.2. The initial buttonSearch_onServerClick() method.

 1: function buttonSearch_onServerClick()

 2: {

 3:    var titleRow = this.form.title1.rowset,

 4:        nCount = 0 ;

 5:    this.form.scopeAll = false ;

 6:    this.form.keywords = this.form.textKeywords.value ;

 7:    this.form.keyfield = "TITLE" ;

 8:    titleRow.canGetRow = KeywordCanGetRow ;

 9:    nCount = titleRow.count() ;

10:    if (nCount == 0) {

11:       var nextForm = new messageForm() ;

12:       nextForm.htmlMessage.text = "<H1>No titles found for: " +

13:       this.form.textKeywords.value + "</H1>" ;

14:    }

15:    else {

16:       var nextForm = new resultsForm() ;

17:       nextForm.titleCount = nCount ;

18:       nextForm.scopeAll = this.form.scopeAll ;

19:       nextForm.keywords = this.form.keywords ;

20:       nextForm.keyfield = this.form.keyfield ;

21:       nextForm.title1.rowset.canGetRow = KeywordCanGetRow ;

22:    }

23:    nextForm.user = this.form.user ;

24:    nextForm.open() ;

25:    this.form.release() ;

26: }


This first incarnation of buttonSearch_onServerClick() provides the property values necessary to get from the Keyword form to the Results form and from the Results form to the Detail form. The logic for selecting the next form to open is similar to that used in the Quick form.

Line 3 makes a shortcut reference to the Title query. This query is identical to those found in the Quick form, the Results form, and the Detail form.


var titleRow = this.form.title1.rowset,

    nCount = 0 ;

Lines 5 through 8 create the four keyword search properties on the current form. The properties must exist on any forms that contain a query using the KeywordCanGetRow() function. The scopeAll, keywords, and keyfield properties work like parameters for the KeywordCanGetRow() function.


this.form.scopeAll = false ;

this.form.keywords = this.form.textKeywords.value ;

this.form.keyfield = "TITLE" ;

titleRow.canGetRow = KeywordCanGetRow ;

The logic for opening the Message form is the same as it is in the Quick form. What might not be apparent is that when line 9 calls Rowset::count(), the KeywordCanGetRow() fires once for every row in the table. If the KeywordCanGetRow() function is simple, the performance is similar to having a filter on a field without an index. Performance degrades as the complexity of the function increases.


nCount = titleRow.count() ;

if (nCount == 0) {

   var nextForm = new messageForm() ;

   nextForm.htmlMessage.text = "<H1>No titles found for: " +

   this.form.textKeywords.value + "</H1>" ;

}

If KeywordCanGetRow() returns true for at least one row, the Results form is set up with the keyword properties. KeywordCanGetRow() is defined within the current script file. Because all scripts are loaded by or included in the Help form, the KeywordCanGetRow() function is available to any part of the application.

Lines 8 and 21 assign the canGetRow event to a function name without parentheses. This establishes a function pointer without calling the function. If you include the parentheses, IntraBuilder evaluates the function and assigns the return value to the event. A return value of true causes an error.


else {

   var nextForm = new resultsForm() ;

   nextForm.titleCount = nCount ;

   nextForm.scopeAll = this.form.scopeAll ;

   nextForm.keywords = this.form.keywords ;

   nextForm.keyfield = this.form.keyfield ;

   nextForm.title1.rowset.canGetRow = KeywordCanGetRow ;

}

The method ends with the standard "open the next form and release this form code." The end code works with both the Message form and the Results form.


nextForm.user = this.form.user ;

nextForm.open() ;

this.form.release() ;

Test the new method through a browser. From the Help form, you can click the Keyword toolbar button to open the Keyword form. The Search button on the Keyword form opens the Results form with every book in the Title table (see Figure 13.9). The Keyword form currently ignores any values you enter in the keyword Text control.

Figure 13.9 : Getting all titles in the results list.

After verifying that you can navigate successfully from the Keyword form to the Results form, reopen the Keyword form in the Script Editor. Replace the current all-too-positive KeywordCanGetRow() function with the one from Listing 13.3. This version returns true if the keyword string is found anywhere in the field value.


Listing 13.3. The initial KeywordCanGetRow() function.

 1: function KeywordCanGetRow()

 2: {

 3:    var lReturn = false,

 4:        thisForm = this.parent.parent ;

 5:    var sKeyValue = thisForm.title1.rowset.fields[thisForm.keyfield].value ;

 6:    if ( sKeyValue.indexOf( thisForm.keywords ) > -1 ) {

 7:       lReturn = true

 8:    }

 9:    return ( lReturn )

10: }


The function starts with the assumption that the current row cannot be retrieved. Line 4 creates a thisForm reference as a shortcut reference to the current form. The canGetValue event belongs to a rowset. When you refer to this inside a rowset event, you are referring to the rowset. The parent or container of a rowset is always a query. The parent of a query is usually a form. When you drop a table onto a form, the query has a parent property pointing to the form. The standard relation between a rowset and a form is that the rowset is the grandchild of the form.

If you create your own instance of a query, it does not need any parent. In benchmarks from Day 10, you made extensive use of queries without parent forms.


var lReturn = false,

    thisForm = this.parent.parent ;

Using the new shortcut reference to the "grandpa" form, line 5 stores the relevant field value to a local variable. This is not creating a reference but creating a new variable, which stores a copy of the original field value.


var sKeyValue = thisForm.title1.rowset.fields[thisForm.keyfield].value ;

The String::indexOf() method determines whether sKeyValue contains the keyword string. String::indexOf() returns the position of one character string within another. Like arrays, string positions are zero based. When the keyword string is not found, String::indexOf() returns -1. Any greater value means the current row can be part of the rowset.


if ( sKeyValue.indexOf( thisForm.keywords ) > -1 ) {

   lReturn = true

}

If the condition has not been met, the default return value excludes the current row from the rowset. This moves the row pointer ahead one row, which fires the canGetRow event again. The process continues until an acceptable row is found or endOfSet is reached.


return ( lReturn )

With this function, you can create more sophisticated searches than are available through query by form. If you search for the word Visual, the result set shows books that contain the word Visual anywhere in the title. This retrieves three titles, as shown in Figure 13.10. Using query by form, only one of the three titles would be found. Your form should now match the Day 13 keyword1.jfm file from the CD-ROM.

Figure 13.10 : The Results form after searching for the keyword Visual.


NOTE
You don't need to worry about shoppers entering keywords containing single quotes. Unlike SQL expressions, the String::indexOf() method is much more forgiving when non-standard string values are passed to it.

Searching with Multiple Fields and Multiple Keywords

To search for more than one non-consecutive keyword, you must be able to identify each keyword as a separate entity. The first version of the Keyword form stored the keywords in a single string variable. The primary difference between the first version and the one you are about to create is that the new version stores each keyword into a separate array element. Having each keyword as a separate array element lets you search for each keyword independently and without regard to the ordering of the keywords. If a shopper enters IntraBuilder Teach, the search needs to locate the title for Teach Yourself IntraBuilder in 21 Days.

While you're at it, you can make the Keyword form work with different fields. The multiple field issue is much simpler to solve than the multiple keyword issue. All you need to do to work with different fields is store the selected field name to the keyfield property. No modifications are needed in the KeywordCanGetRow() function to work with multiple fields.

This version on the Keyword form ignores the Match all keywords checkbox and operates as if it is not checked. Books will be included in the rowset if any keywords are found. The final Keyword form will allow shoppers to choose to see only books that contain either any listed keyword or every listed keyword.

Creating an Array of Keywords

The keyword array can use the same this.form.keywords reference that was used for the keyword string. The source for the array is this.form.textKeywords.value. Rewrite the buttonSearch_onServerClick() method in order to separate all the words in this.form.textKeywords.value. As in most English sentences, the single space serves as the delimiter between words. Use Listing 13.4 to modify the method with the Script Editor.


Listing 13.4. The multiple keyword buttonSearch_onServerClick() method.

 1: function buttonSearch_onServerClick()

 2: {

 3:    var titleRow = this.form.title1.rowset,

 4:        nCount = 0,

 5:        nSpace = 0,

 6:        sWords = new StringEx() ;

 7:    // scopeAll

 8:    this.form.scopeAll = false ;

 9:    // keywords

10:    this.form.keywords = new Array() ;

11:    sWords.string = this.form.textKeywords.value ;

12:    while ( sWords.length > 0 ) {

13:       nSpace = sWords.indexOf(" ") ;

14:       if ( nSpace > -1 ) {

15:          this.form.keywords.add( sWords.left( nSpace++ ) ) ;

16:          sWords.string = sWords.right( sWords.length - nSpace ) ;

17:       }

18:       else {

19:          this.form.keywords.add( sWords.string ) ;

20:          sWords.string = null ;

21:       }

22:    }

23:    // keyfield

24:    if ( this.form.radioAuthor.value ) {

25:       this.form.keyfield = "AUTHOR" ;

26:    }

27:    else if ( this.form.radioISBN.value ) {

28:       this.form.keyfield = "ISBN" ;

29:    }

30:    else {

31:       this.form.keyfield = "TITLE" ;

32:    }

33:   // canGetRow

34:    titleRow.canGetRow = KeywordCanGetRow ;

35

36:    nCount = titleRow.count() ;

37:    if (nCount == 0) {

38:       var nextForm = new messageForm() ;

39:       nextForm.htmlMessage.text = "<H1>No titles found for: " +

40:          this.form.textKeywords.value + "</H1>" ;

41:    }

42:    else {

43:       var nextForm = new resultsForm() ;

44:       nextForm.titleCount = nCount ;

45:       nextForm.scopeAll = this.form.scopeAll ;

46:       nextForm.keywords = this.form.keywords ;

47:       nextForm.keyfield = this.form.keyfield ;

48:       nextForm.title1.rowset.canGetRow = KeywordCanGetRow ;

49:    }

50:    nextForm.user = this.form.user ;

51:    nextForm.open() ;

52:    this.form.release() ;

53: }


NOTE
The basic structure of the buttonSearch_onServerClick() method does not change between versions. It starts by assigning the search properties to the current form. Then, the method counts to see how many rows exist. The last chore is to select the next form and open it. The only modifications take place on the lines that set up the search properties. In this version, the setup logic spans from line 7 to line 34. Each buttonSearch_onServerClick() method uses progressively more complex logic to set up properties.

The var statement initializes two new variables: nSpace and sWords. The nSpace variable keeps track of space delimiter position. The sWords variable acts as the scratch pad for dividing the string. As a StringEx object, it has all the normal functionality of a String object along with some additional methods, including StringEx::left() and StringEx::right().


var titleRow = this.form.title1.rowset,

    nCount = 0,

    nSpace = 0,

    sWords = new StringEx() ;

Line 10 creates the keywords property as an empty array. If no words are found in this.form.textKeywords.value, the array stays empty. If the method results in an empty array, the KeywordCanGetRow() function returns false for every row and no books are found.


// keywords

this.form.keywords = new Array() ;

The value of the Text control is itself a String object. That String object is assigned to the string property of sWords. Although standard String objects also have a string property, the property is relevant only to a StringEx object. When dealing with existing String objects, myString = "Hello World" is identical in function to myString.string = "Hello World". The resulting myString is the same. If myString were a StringEx object, myString = "Hello World" would release the StringEx object and create a new standard String object in its place.


sWords.string = this.form.textKeywords.value ;

The while loop is used to work through sWords. The loop continues until sWords becomes null or an empty string. Using this kind of logic, something inside the loop must shrink down the size of sWords if you want to avoid an infinite loop. Line 13 looks for a space in sWords and stores the position to nSpace.


while ( sWords.length > 0 ) {

   nSpace = sWords.indexOf(" ") ;

If a space is found, the method divides sWords into two pieces. Any characters to the left of the space are considered a word. Line 15 creates a new element of the keywords array containing the current word. Line 16 takes everything to the right of the space and stores it to the string property of sWords.


if ( nSpace > -1 ) {

   this.form.keywords.add( sWords.left( nSpace++ ) ) ;

   sWords.string = sWords.right( sWords.length - nSpace ) ;

}

In the preceding code, the ++ increment operator adds one to nSpace after StringEx::left() executes and before the StringEx::right() executes. Without the ++ operator, the space would remain in the string and an infinite loop would result. Another way to accomplish the same task would be to prefix nSpace with ++ in the second assignment statement.

When ++ is a suffix, the incrementing does not take place until after all the other parts of the statement execute. This is why the nSpace++ has no effect on the value passed to StringEx::left(). As a prefix, the opposite occurs. In the following code, the increment happens before nSpace is subtracted from sWords.length.


if ( nSpace > -1 ) {

   this.form.keywords.add( sWords.left( nSpace ) ) ;

   sWords.string = sWords.right( sWords.length - ++nSpace ) ;

}

The StringEx::left() and StringEx::right() methods are shorthand for a substring operation. If you want to use a String class for sWords, the same code could be written as follows. With String::substring(), you must pass two parameters instead of one.


 if ( nSpace > -1 ) {

   this.form.keywords.add( sWords.substring(0, nSpace++ ) ) ;

   sWords.string = sWords.substring( nSpace, sWords.length ) ;

}

If there are no more spaces in sWords, then sWords has the last word. After the last word is added as an array element, line 20 stores null to the string property. The length of the null string is zero, forcing the while loop to end.


else {

   this.form.keywords.add( sWords.string ) ;

   sWords.string = null ;

}

A series of if blocks sets the keyfield property. When a radio button is selected, the value property becomes true. The routine has a slight optimization in that it never checks to see whether the this.form.radioTitle value is true. If neither of the other two is selected, the title option must be selected.


// keyfield

if ( this.form.radioAuthor.value ) {

   this.form.keyfield = "AUTHOR" ;

}

else if ( this.form.radioISBN.value ) {

   this.form.keyfield = "ISBN" ;

}

else {

   this.form.keyfield = "TITLE" ;

}

NOTE
Unlike other controls, the value property of a radio button is not equal to the value of an associated dataLink. The value property of the radio button is a logical value; however, its dataLink can point only to a character string. The value of the dataLink is equal to the text of the radio button.

Searching Through an Array of Keywords

The Keyword form will not run correctly if buttonSearch_onServerClick() creates the keywords property as an array and KeywordCanGetRow() tries to use it as a character string. To remedy the situation, modify KeywordCanGetRow() as shown in Listing 13.5.


Listing 13.5. The multiple keyword KeywordCanGetRow() function.

 1: function KeywordCanGetRow()

 2: {

 3:    var lReturn = false,

 4:        thisForm = this.parent.parent ;

 5:    var sKeyValue=thisForm.title1.rowset.fields[thisForm.keyfield].value;

 6:

 7:    for ( var i = 0 ; i < thisForm.keywords.length ; i++ ) {

 8:       if (sKeyValue.indexOf( thisForm.keywords[i] ) > -1 ) {

 9:          lReturn = true ;

10:          break ;

11:       }

12:    }

13:

14:    return ( lReturn )

15: }


The variable declaration and return statements are identical to those found in the previous KeywordCanGetRow() function. The new statements are on lines 7 through 12 where a loop cycles through each element of the keywords array. Every array element is a keyword to seek in sKeyValue. Whenever the key is found, the return flag is set to true.


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

   if (sKeyValue.indexOf( thisForm.keywords[i] ) > -1 ) {

      lReturn = true ;

      break ;

   }

}

There is no need to continue the search after a keyword has been found. The break statement on line 10 exits the for loop as soon as the first keyword is found.

Your form should now match the Day 13 keyword2.jfm file from the CD-ROM. With this version complete, you can perform a wide range of searches. You could simultaneously look for all books on C++ and Delphi. Figure 13.11 shows the Keyword form set up to look for books listed with Mahar or Swan in the Author field. A keyword search with this criteria returns the following books:

Figure 13.11 : Searching for books by Mahar or Swan.


dBASE for Windows Unleashed

Teach Yourself IntraBuilder in 21 Days

Tom Swan's Mastering Borland C++ 5

Visual dBASE 5.5 Unleashed

Adding Search Rule Options

The second version of the Keyword form is pretty good, but it has some bugs. If you enter more than one space between keywords, the extra space is treated as a keyword. This results in every book getting returned if the search is on the Title or Author fields.

Although it is not necessarily considered a bug, the searches are case sensitive. If shoppers enter Dbase, they get the message shown in Figure 13.12. To make it easy on shoppers, the system should be case insensitive. After all, how many shoppers will be able to remember whether dBASE is capitalized as Dbase, dbase, or DBASE?

Figure 13.12 : The results of searching for Dbase with a case-sensitive search.

Along with these bug fixes, the final version of the Keyword form will incorporate changes needed for the Match all keywords option. Fire up the Script Editor and modify buttonSearch_onServerClick() as shown in Listing 13.6 and KeywordCanGetRow() as shown in Listing 13.7.


Listing 13.6. The final version of the buttonSearch_onServerClick() method.

 1: function buttonSearch_onServerClick()

 2: {

 3:    var titleRow = this.form.title1.rowset,

 4:        nCount = 0,

 5:        nSpace = 0,

 6:        sWords = new StringEx() ;

 7:    // scopeAll

 8:    this.form.scopeAll = this.form.checkAll.checked ;

 9:    // keywords

10:    this.form.keywords = new Array() ;

11:    sWords.string = this.form.textKeywords.value ;

12:    sWords.string = sWords.toUpperCase() ;

13:    sWords.string = sWords.leftTrim() ;

14:    sWords.string = sWords.rightTrim() ;

15:    while ( sWords.length > 0 ) {

16:       nSpace = sWords.indexOf(" ") ;

17:       if ( nSpace > -1 ) {

18:          this.form.keywords.add( sWords.left( nSpace++ ) ) ;

19:          sWords.string = sWords.right( sWords.length - nSpace );

20:          sWords.string = sWords.leftTrim() ;

21:       }

22:       else {

23:          this.form.keywords.add( sWords.string ) ;

24:          sWords.string = null ;

25:       }

26:     }

27:     // keyfield

28:     if ( this.form.radioAuthor.value ) {

29:        this.form.keyfield = "AUTHOR" ;

30:     }

31:     else if ( this.form.radioISBN.value ) {

32:        this.form.keyfield = "ISBN" ;

33:     }

34:     else {

35:        this.form.keyfield = "TITLE" ;

36:     }

37:   // canGetRow

38:     titleRow.canGetRow = KeywordCanGetRow ;

39:

40:     nCount = titleRow.count() ;

41:     if (nCount == 0) {

42:        var nextForm = new messageForm() ;

43:        nextForm.htmlMessage.text = "<H1>No titles found for: "+

44:           this.form.textKeywords.value + "</H1>" ;

45:     }

46:     else {

47:        var nextForm = new resultsForm() ;

48:        nextForm.titleCount = nCount ;

49:        nextForm.scopeAll = this.form.scopeAll ;

50:        nextForm.keywords = this.form.keywords ;

51:        nextForm.keyfield = this.form.keyfield ;

52:        nextForm.title1.rowset.canGetRow = KeywordCanGetRow ;

53:     }

54:     nextForm.user = this.form.user ;

55:     nextForm.open() ;

56:     this.form.release() ;

57: }


The final changes to buttonSearch_onServerClick() are minor compared to the final changes to KeywordCanGetRow(). Line 8 contains one change, and lines 12, 13, 14, and 22 are new. Line 8 now uses the checkbox value. If a shopper has checked the checkbox, scopeAll becomes true, and all keywords must match in order for a row to be accepted.


// scopeAll

this.form.scopeAll = this.form.checkAll.checked ;

NOTE
The checked property of a checkbox is similar to the value property of a Text control. If a checkbox has a dataLink, the field value is equal to the checked property. IntraBuilder can reflect changes to the dataLink field value into the checked property.

Lines 12 through 14 use methods of sWords to modify its own string property. String methods return new values without changing any properties of their related string. You can assign the results of a string method to the string property to alter an existing String object. String::toUpperCase() is available in the standard String class and the StringEx class. The two trimming methods exist only in the StringEx class.


sWords.string = this.form.textKeywords.value ;

sWords.string = sWords.toUpperCase() ;

sWords.string = sWords.leftTrim() ;

sWords.string = sWords.rightTrim() ;

At this point all keywords are in uppercase, and excess spaces are trimmed from the ends of the keyword list. To take care of multiple spaces between words, line 22 calls StringEx::leftTrim() each time a new word is extracted from the string.


if ( nSpace > -1 ) {

   this.form.keywords.add( sWords.left( nSpace++ ) ) ;

   sWords.string = sWords.right( sWords.length - nSpace ) ;

   sWords.string = sWords.leftTrim() ;

}

These changes are all you need to do for buttonSearch_onServerClick(). Because the final version makes all the keywords uppercase, it will not work correctly with the previous version of KeywordCanGetRow().


Listing 13.7. The third and final KeywordCanGetRow() function.

 1: function KeywordCanGetRow()

 2: {

 3:    var thisForm = this.parent.parent,

 4:        nHits = 0 ;

 5:    var sKeyValue =

 6:    thisForm.title1.rowset.fields[thisForm.keyfield].value.toUpperCase();

 7:

 8:    for ( var i = 0 ; i < thisForm.keywords.length ; i++ ) {

 9:       if ( sKeyValue.indexOf( thisForm.keywords[i] ) > -1 ) {

10:          nHits++ ;

11:       }

12:    }

13:

14:    return thisForm.scopeAll ? nHits==thisForm.keywords.length : nHits>0;

15: }


NOTE
This version of KeywordCanGetRow() is significantly different from the prior version. To start, the lReturn variable is gone, and line 4 creates a new nHits variable. The function uses nHits to track how many of the keywords can be found.


var thisForm = this.parent.parent,

    nHits = 0 ;

Lines 5 and 6 comprise a single statement that converts the key field value to uppercase and assigns the result to sKeyValue. The value property of a character field is also a String class. Having the field value and the keywords in uppercase removes all case sensitivity.


var sKeyValue =

 thisForm.title1.rowset.fields[thisForm.keyfield].value.toUpperCase() ;

The lReturn flag is replaced by the nHits counter when a keyword is found. Counting the number of hits is required for the scopeAll option.


if ( sKeyValue.indexOf( thisForm.keywords[i] ) > -1 ) {

   nHits++ ;

}

The break statement also has been removed. Although this does create some redundant processing when scopeAll is false, any alternative could slow down both types of searches. For instance, you could check the scopeAll property to see whether a break is possible as shown in the following code. This can speed up searches that match on any word and use more than three words. It slows down all other searches.


if ( sKeyValue.indexOf( thisForm.keywords[i] ) > -1 ) {

   nHits++ ;

   if ( ! thisForm.scopeAll ) {

       break ;

   }

}

The return statement on line 14 is fairly compressed. For most routines, a standard if block is preferable for readability. However, canGetRow event logic is close to being the definition of performance challenged. Using a single expression to calculate the return value makes for the fastest possible processing.


return thisForm.scopeAll ? nHits == thisForm.keywords.length : nHits > 0 ;

If the scopeAll property is true, the rowset is only included if the number of hits matches the number of keywords in the array. Because nHits can only increment once for each keyword, if nHits equals the array length, every keyword has been found. If scopeAll is false, the fact that there were any hits is good enough for a row to be in the rowset. Using a standard if block, the same logic would look like this:


if ( thisForm.scopeAll ) {

   return ( nHits == thisForm.keywords.length ) ;

}

else {

   return ( nHits > 0 ) ;

}

Rejoice, for the Keyword form is done. Shoppers now have an intuitive way to make complex searches. Try it and see how the form responds with different scopeAll settings. Figure 13.13 shows a search for titles that contain DELPHI and 2. If the Match all keywords option is checked, the result is four books about Delphi 2. When the Match all keywords option is not checked, the result has nine books, including a book on Delphi 1 and all the books in the Teach Yourself in 21 Days series.

Figure 13.13 : Searching for books about Delphi 2.

If your form is not working as expected, compare it to Listing 13.8. The listing shows the complete source code for the final version of the Keyword form. This version is also available as the Day 13 keyword3.jfm file on the CD-ROM.


Listing 13.8. The complete source for the Keyword form.

  1: // {End Header} Do not remove this comment//

  2: // Generated on 01/02/97

  3: //

  4: var f = new keywordForm();

  5: f.open();

  6: class keywordForm extends ToolbarCForm from "TOOLBAR.JCF" {

  7:    with (this) {

  8:       onServerLoad = {;this.buttonKeyword.visible = false ;};

  9:       height = 15;

 10:       left = 0;

 11:       top = 0;

 12:       width = 75;

 13:       title = "Boingo's Books - Keyword Search";

 14:    }

 15: 

 16:    with (this.title1 = new Query()){

 17:      left = 70;

 18:      top = 2;

 19:      sql = 'SELECT * FROM "title.DBF"';

 20:      active = true;

 21:   }

 22: 

 23:   with (this.title1.rowset) {

 24:   }

 25: 

 26:   with (this.rule3 = new Rule(this)){

 27:      top = 10;

 28:      size = 2;

 29:      right = 70;

 30:   }

 31: 

 32:   with (this.htmlHelp = new HTML(this)){

 33:      height = 2;

 34:     left = 1;

 35:     top = 3.5;

 36:     width = 65;

 37:     color = "black";

 38:     text = 'To search the book store, type keywords ' + 

 39:            'in the box below separated by a space, ' + 

 40:            'then click the "Search" button.';

 41:   }

 42: 

 43:   with (this.textKeywords = new Text(this)){

 44:      left = 1;

 45:      top = 5.5;

 46:      width = 55;

 47:      value = "";

 48:    }

 49: 

 50:   with (this.radioTitle = new Radio(this)){

 51:      height = 1;

 52:      left = 1;

 53:      top = 6.5;

 54:      width = 20;

 55:      text = "Search by Title";

 56:      value = true;

 57:      groupName = "keyfield";

 58:   }

 59: 

 60:   with (this.radioAuthor = new Radio(this)){

 61:      height = 1;

 62:      left = 1;

 63:      top = 7.5;

 64:      width = 20;

 65:      text = "Search by Author";

 66:      value = false;

 67:      groupName = "keyfield";

 68:   }

 69: 

 70:   with (this.radioISBN = new Radio(this)){

 71:      height = 1;

 72:      left = 1;

 73:      top = 8.5;

 74:      width = 20;

 75:      text = "Search by ISBN";

 76:      value = false;

 77:      groupName = "keyfield";

 78:   }

 79:

 80:   with (this.checkAll = new CheckBox(this)){

 81:      height = 1;

 82:      left = 1;

 83:      top = 10.25;

 84:      width = 20;

 85:      text = "Match all keywords";

 86:      checked = false;

 87:   }

 88:

 89:   with (this.buttonSearch = new Button(this)){

 90:      onServerClick = class::buttonSearch_onServerClick;

 91:      left = 1;

 92:      top = 11.5;

 93:      width = 12;

 94:      text = "Search";

 95:   }

 96:

 97:   with (this.reset1 = new Reset(this)){

 98:      left = 15;

 99:      top = 11.5;

100:      width = 12;

101:      text = "Reset";

102:   }

103:

104:   this.rowset = this.title1.rowset;

105:

106:   function buttonSearch_onServerClick()

107:   {

108:      var titleRow = this.form.title1.rowset,

109:          nCount = 0,

110:          nSpace = 0,

111:          sWords = new StringEx() ;

112:      // scopeAll

113:      this.form.scopeAll = this.form.checkAll.checked ;

114:      // keywords

115:      this.form.keywords = new Array() ;

116:      sWords.string = this.form.textKeywords.value ;

117:      sWords.string = sWords.toUpperCase() ;

118:      sWords.string = sWords.leftTrim() ;

119:      sWords.string = sWords.rightTrim() ;

120:      while ( sWords.length > 0 ) {

121:         nSpace = sWords.indexOf(" ") ;

122:         if ( nSpace > -1 ) {

123:            this.form.keywords.add( sWords.left( nSpace++ ) ) ;

124:            sWords.string = sWords.right( sWords.length - nSpace );

125:            sWords.string = sWords.leftTrim() ;

126:         }

127:         else {

128:            this.form.keywords.add( sWords.string ) ;

129:            sWords.string = null ;

130:         }

131:      }

132:      // keyfield

133:      if ( this.form.radioAuthor.value ) {

134:         this.form.keyfield = "AUTHOR" ;

135:      }

136:      else if ( this.form.radioISBN.value ) {

137:         this.form.keyfield = "ISBN" ;

138:      }

139:      else {

140:         this.form.keyfield = "TITLE" ;

141:      }

142:      // canGetRow

143:      titleRow.canGetRow = KeywordCanGetRow ;

144:            nCount = titleRow.count() ;

145:      if (nCount == 0) {

146:         var nextForm = new messageForm() ;

147:         nextForm.htmlMessage.text = "<H1>No titles found for: "+

148:            this.form.textKeywords.value + "</H1>" ;

149:      }

150:      else {

151:         var nextForm = new resultsForm() ;

152:         nextForm.titleCount = nCount ;

153:         nextForm.scopeAll = this.form.scopeAll ;

154:         nextForm.keywords = this.form.keywords ;

155:         nextForm.keyfield = this.form.keyfield ;

156:         nextForm.title1.rowset.canGetRow = KeywordCanGetRow ;

157:      }

158:      nextForm.user = this.form.user ;

159:      nextForm.open() ;

160:      this.form.release() ;

161:   }

162:

163: }

164:

165: function KeywordCanGetRow()

166: {

167:    var thisForm = this.parent.parent,

168:        nHits = 0 ;

169:    var sKeyValue =

170:  thisForm.title1.rowset.fields[thisForm.keyfield].value.toUpperCase();

171: 

172:    for ( var i = 0 ; i < thisForm.keywords.length ; i++ ) {

173:      if ( sKeyValue.indexOf( thisForm.keywords[i] ) > -1 ) {

174:          nHits++ ;

175:       }

176:    }

177: 

178:    return thisForm.scopeAll ? nHits==thisForm.keywords.length:nHits>0;

179: }


Don't try to run the Keyword form by itself. The search routine requires that the user property be set on the Keyword form. The user property is set only when the application is started from the Help form.

Preparing to Check Out

To get ready for the final day of shopping cart application building, create a stub version of the Checkout form and add it to the store.h file. This will take care of the error that occurs when shoppers click on the Checkout toolbar button.

  1. Open the Form Designer from the Script Pad. Use the following JavaScript statement:
    _sys.forms.design("checkout.jfm") ;
  2. Make sure the toolbar custom form class is set as the active custom form class.
  3. Change the form's title property to Boingo's Books - Checkout, as shown in Figure 13.14.
    Figure 13.14 : Setting up a stub version of the Checkout form.

  4. Save the form and close the Form Designer.
  5. Open store.h in the Script Editor and add load and include statements for the Checkout form. When you have the form looking like Listing 13.9, save it and close IntraBuilder.
  6. Reopen IntraBuilder and force a recompile of the Help form by selecting Compile from the IntraBuilder Explorer shortcut menu.


Listing 13.9. The store.h file with support for the Checkout form.

 1: #define DEBUG // for development only

 2: #ifndef STORE

 3:    #define STORE

 4:    #ifdef DEBUG

 5:       // Load scripts

 6:       // _sys.script.load statements go here.

 7:       _sys.scripts.load("cart.jfm") ;

 8:       _sys.scripts.load("checkout.jfm") ;

 9:       _sys.scripts.load("detail.jfm") ;

10:       _sys.scripts.load("help.jfm") ;

11:       _sys.scripts.load("keyword.jfm") ;

12:       _sys.scripts.load("message.jfm") ;

13:       _sys.scripts.load("quick.jfm") ;

14:       _sys.scripts.load("results.jfm") ;

15:    #elseif

16:       // Include scripts

17:       return null

18:       // #include statements go here

19:       #include <cart.jfm>

20:       #include <checkout.jfm>

21:       #include <detail.jfm>

22:       #include <help.jfm>

23:       #include <keyword.jfm>

24:       #include <message.jfm>

25:       #include <quick.jfm>

26:       #include <results.jfm>

27:    #endif

28: #endif


The only changes are on lines 8 and 20 where the Checkout form is listed with the other application forms. Compiling in this header allows all the toolbar buttons to work without error. Each toolbar button opens a different form, and this header is the first one to include all the form filenames.

That is all for today. Tomorrow you can dive directly into the Checkout form development without having to worry about integrating the new form into the Help form initialization.

Summary

Today you learned about controlling a form's HTML layout and how to create a complex filter through the rowset canGetRow event. Throughout the day, you created three versions of the Keyword search form.

The day began with the visual layout of all the controls on the Keyword search form. After you had a professional-looking Keyword form, you gradually added more complex options to a function linked to a canGetRow event. The first version allowed searches for a single word on a single field. The second could search for multiple words on multiple fields. The final version was case insensitive and allowed the user to specify whether all or any of the search words are required.

After completing the Search form, you created a stub version of the Checkout form. Tomorrow, you will complete the Checkout form, and with that, you will complete the shopping cart application.

Q&A

Q:I am unable to enter some of the position property values as given. When I enter a top value of 10.25 into the Inspector, it changes to 10.23. If I use the Script Editor and change the value to 10.25, it is overwritten with 10.22 as soon as I make any new changes with the Form Designer. What is going on? Should I stress over this?
A:The Form Designer coordinate system is based on the system display font. If you change resolutions for system font size, the allowable values change slightly. There is no need to stress over the differences. The relative position of controls is much more critical than the actual position. Do not worry whether a control has a top position of 10.25 or 10.23.
Q:When using the filter property or the Rowset::applyLocate() method, table type and indexing made a big difference. Are there similar factors to consider when using canGetRow?
A:The filter and Rowset::applyLocate() methods pass off the query processing details to the database engine. The table type you are using determines whether the query is run by BDE, ODBC, or a remote SQL server. The canGetRow shifts the query processing into IntraBuilder. The only thing the database engine does for canGetRow is the low overhead task of moving the row pointer. The best way to speed up a canGetRow event is to streamline your JavaScript and remove any excess processing.
Q:If I create a rule line through the Navigator Form wizard, I can select a wide range of line styles including some that have moving images. In the IntraBuilder Form Designer, the only option to change the rule style seems to be the size property. How can I create more exciting rule lines with IntraBuilder?
A:When you select an exotic rule line in the Navigator Form wizard, the generated HTML contains an image in place of the standard HTML rule tag. If you have an image file with the rule style you want to use, you can use the same approach with IntraBuilder. After you create a form with the Navigator Form wizard and store its associated GIF to a local folder, you can use the GIF with an IntraBuilder image control. The GIF files for rules have names like rule07.gif.
Q:My boss says I always have to use object-oriented programming. Using KeywordCanGetRow() as a standard function that resides outside of a class seems as though it is breaking the rules. Is there any way to use KeywordCanGetRow() as a method instead of a function?
A:Yes, you can use the scope resolution operator to call a method, which is defined within another class. If you move the KeywordCanGetRow() inside the keywordForm class, you only need to change three other lines to use it as a method. The following excerpt shows the changes.
In keyword.jfm, change:
titleRow.canGetRow = KeywordCanGetRow ;
To:
titleRow.canGetRow = keywordForm::KeywordCanGetRow ;
In both keyword.jfm and results.jfm, change:
nextForm.title1.rowset.canGetRow = KeywordCanGetRow ;
To:
nextForm.title1.rowset.canGetRow = keywordForm::KeywordCanGetRow

Workshop

The Workshop section provides questions and exercises to help you get a better feel for the material you learned today. Try to answer the questions and at least think about the exercises before moving on to tomorrow's lesson. You'll find the answers to the questions in Appendix A, "Answers to Quiz Questions."

Quiz

  1. How do you reference a form from a rowset event?
  2. What is the HTML tag for a line break?
  3. How can you create more than one set of radio buttons in a single form?
  4. What is the proper capitalization of Borland's DBF-oriented desktop database?
  5. What class contains the toUpperCase() method?
  6. How can you force any form to use a browser-defined color scheme in Navigator?

Exercises

  1. Try replacing the checkAll checkbox in the Keyword form with two radio buttons. Remember that the groupName property of the new radio buttons must distinguish them from the form's existing radio buttons.
  2. As presented here, the final Keyword form does not have very elegant handling of a blank keyword list. If the Match all keywords option is checked, every book comes back as found. If the option is not checked, no books are found. Add a routine to check for a blank keyword list in buttonSearch_onServerClick(). If the blank list is found, open the Message form with an appropriate message.