Day 14

Checking Out

by Paul Mahar


CONTENTS

Today you will finish up the shopping cart application and learn how to validate data, work with form pages, and create a Receipt report. By the end of the day you will have a fully functional on-line order entry system. There are just two main tasks for the day:

Both the Checkout form and the Receipt report demonstrate new ways to pass information between application objects. So far, the application has created custom properties, such as the this.form.user, to pass information from one form to the next. The Checkout form uses pages to combine the features of several forms into a single JavaScript form definition. This allows each page to appear as a separate form yet have access to the same set of properties and methods. The Receipt report receives information through a parameter list.

Designing the Checkout Form

The purpose of the Checkout form is to accept address and credit card information from shoppers when they want to make a purchase. To support this, the Checkout form needs some data entry controls with datalinks to the Customer table. To provide validation, the form needs to prevent two possible error conditions. First, the form needs to prevent any data entry when no items are in the cart. Second, it must validate the entered data when items are in the cart. To accomplish this, the Checkout form works like three forms in one, each functional form appearing on one of the following pages.

Although data from only one table ever appears on the Checkout form, it works with the following five tables. Two of the tables appear in many forms, but three are new to this form.

Designing the Data Entry Page

The first and most complex page is for customer data entry. Page one contains controls for all but one field of the Customer table. The hidden field is the customer number field. The value for the customer number does not need to be calculated until all the other fields are validated.

Three field groupings exist in the Customer table. The first group consists of the customer name and phone number. The address group has all the common address fields and a country field for international orders. The last group contains fields for credit card information.

NOTE
Many order systems allow for separate billing and shipping addresses. You could expand on the Customer table and Checkout form to contain both "bill to" and "ship to" fields. If you do, also add a checkbox to default them to being the same address.

Most of the fields are simple string values that work best as datalinks to Text controls. The exceptions are where you can limit the shopper to a list of valid choices. The bookstore can accept orders from a limited number of countries. Because the number of countries is greater than five, a select control is a good choice for the country field. Using a select control also makes it easy to expand the country list as the store expands. Boingo will accept only the three major credit cards. You can use a set of radio buttons to represent each type of credit card.

Along with all the controls for the Customer table, the customer data entry page can work with the standard toolbar buttons and two new buttons: one button to commit the purchase and another to cancel the Checkout process. Follow these steps to design the first page of the Checkout form.

  1. Open the Checkout form in the Design mode.
  2. Drop the following tables from the IntraBuilder Explorer onto the Checkout form: Cart.dbf, Customer.dbf, Invoice.dbf, Lineitem.dbf, and Title.dbf.
  3. Remove the full path from the sql properties of the five new queries. Inspect each query and modify the SQL commands so that they match the following.
    SELECT * FROM "customer.DBF"
    SELECT * FROM "cart.DBF"
    SELECT * FROM "lineitem.DBF"
    SELECT * FROM "invoice.DBF"
    SELECT * FROM "title.DBF"
  4. Relate the title1 query to the cart1 query. Select the title1 query and descend its rowset property. Use the drop-down list on masterRowset to select the cart1 query. Use the drop-down list on masterFields to select ISBN. The indexName is automatically set to ISBN, as shown in Figure 14.1.
    Figure 14.1 : Relating the Title query to the Cart query.

  5. Add three HTML headings, as shown in Table 14.1. Each heading labels the group of fields that will appear to the right. As minor level-four headings, the <H4> font change might not be perceivable in the Designer.

Table 14.1. Minor headings for the field groups.

NameText
Height
Left
Top
Width
headCustomer<H4>Customer</H4>
1
1
4.5
10
headAddress<H4>Address</H4>
1
1
6.5
10
headCard<H4>Credit Card</H4>
1
1
9.5
10

  1. Add nine HTML field labels and configure them, as shown in Table 14.2. In this layout, each field group is horizontally separated by half a space. This division is more visible in the Designer than in a browser. All but the State label appears in the same column. The label layout leaves room for a set of radio button controls that you will later add above the card number. After you add the field labels, your form should resemble the one shown in Figure 14.2.
    Figure 14.2 : Adding field labels to the Checkout form.

Table 14.2. Field labels for the Customer table.

NameText
Height
Left
Top
Width
labelNameName
1
12
4.5
11.5
labelPhonePhone
1
12
5.5
11.5
labelStreetStreet
1
12
6.5
11.5
labelCityCity
1
12
7.5
11.5
labelStateState
1
60
7.5
5
labelPostalPostal Code
1
12
8.5
11.5
labelCNumberCart #
1
12
10.5
11.5
labelCNameName on Card
1
12
11.5
11.5
labelCDateExp. Date
1
12
12.5
11.5

NOTE
Depending on the current display configuration, the entire text for labelCName might not be visible in the Designer. This does not affect how the HTML displays in a browser.

  1. Open the Field Palette and add fields from the customer1 tab. When you drop controls from the Field Palette, the control names match the field names. Leave the given names and position the fields, as shown in Table 14.3.

Table 14.3. Datalinked controls from the Field Palette.

Name
Left
Top
Width
name
29
4.5
40
phone
29
5.5
20
street
29
6.5
40
city
29
7.5
30
state
66
7.5
3
postal
29
8.5
12
cardnumber
29
10.5
20
cardname
29
11.5
40
carddate
29
12.5
12

TIP
When you're working with large tables, the screen can quickly get cluttered by the Field Palette. You can shrink down the Field Palette by showing text only. This is an option in the Toolbars and Palettes dialog box.

  1. Before adding any more controls, save the form and run it through both Navigator and Internet Explorer. If you need to make any adjustments, it is better to do that now rather than later. Figure 14.3 shows how the controls align in Navigator.
    Figure 14.3 : Viewing the Text control layout in Netscape Navigator.

At this point, the Checkout form is void of logic and works without the rest of the shopping cart application. You can run it directly using the following URL:


http://localhost/svr/intrasrv.isv?store/checkout.jfm

The form still needs one more HTML control, two more buttons, three radio buttons, and a select control. The Checkout toolbar button also is inappropriate on the Checkout form. If you have the existing controls looking appropriate, continue on to finish up the layout of page one.

  1. Reopen the Checkout form in the Form Designer.
  2. Add an HTML control to display the total purchase price for items currently in the Cart. Use the properties listed in Table 14.4. You can leave the text property as a default. The form's onServerLoad event takes care of assigning the correct string to this control's text property.

Table 14.4. Properties for the htmlTotal control.

PropertyValue
namehtmlTotal
height1
left1
top3.5
width65

  1. Add three radio buttons, using the properties shown in Table 14.5. Note that the radio buttons might appear with a bold font when first added to a form. However, when you run or redesign the form, the radio buttons display with the correct font.

Table 14.5. Datalinked controls from the Field Palette.

namegroupName text
height
left
top
width
radioVisacardVisa
1
12
9.5
11.5
radioMCcardMaster Card
1
29
9.5
12
radioAMEXcardAmerican Express
1
46
9.5
23

The radio buttons correspond to the Card field in the Customer table. Although you could create a direct datalink from the field to each radio button, the field length does not accommodate the current text properties. The field can store only up to 10 characters. Rather than storing the longer strings, the application will store VISA, MC, and AMEX to the field.

  1. Add a select control with the properties shown in Table 14.6. This is the drop-down version, unlike the select list used in the Quick form and the Results form.

Table 14.6. Properties for the selectCountry control.

PropertyValue
nameselectCountry
left46
top8.5
width23

  1. Datalink the selectCountry to the Country field in the Customer table. Use the visual property builder shown in Figure 14.4. Open the dialog box by clicking on the dataLink property tool button.
    Figure 14.4 : DataLinking selectCountry to parent.customer1. rowset.fields [³COUNTRY²].

  2. Attach an array to the Select control's options property. Click on the Options tool button to select array and click on the Array tool button to open the array builder.
  3. Create the literal array by entering country names under String and adding them to the array. Use USA as the first element, so that it becomes the default. Add five or six more countries, as shown in Figure 14.5.
    Figure 14.5 : Building an array for the options property.

  4. Add two buttons at the bottom of the form. Use the properties listed in Table 14.7.

Table 14.7. Datalinked controls from the Field Palette.

NameText
Left
Top
Width
buttonBuyBuy Now
1
13.5
12
buttonCancelCancel
15
13.5
12

  1. Before you can view the final appearance in a browser, you must hide the Checkout toolbar button. Inspect form and click the tool button for the onServerLoad event. Create a simple method as shown:
    function Form_onServerLoad()
    {
    this.buttonCheckout.visible = false ;
    }

With all the visual design work done on page one, save the form and run it in the IntraBuilder Designer. Try using the select drop-down list and pay attention to the tab order. When you tab through the form in the Designer, the order matches the order in which you added controls in the Form Designer. The first controls are the toolbar buttons. The tab moves to the Text controls and then to the first radio button. Notice that the order does not flow from top to bottom. From the radio button, focus goes to the select list.

After you know the Checkout form runs okay in the Designer, try it in both Netscape Navigator and Internet Explorer. The tab order in both browsers goes from top to bottom. Internet Explorer treats the radio button group as a single tab stop and includes the URL field in the tab set of the form. Navigator treats each radio button as a separate tab and does not include the URL entry in the same tab set as form controls. Navigator has no default tab position.

Figure 14.6 : Using the Select control in Internet Explorer.

The Select control behavior is slightly different when run through a browser than when run through the Designer. In the Designer, the value of the control can differ from any elements in the list. Navigator and Internet Explorer render the Select control as a drop-down list box that forces all values to exist in the list. If a datalink value is not in the list, the value changes to the first item in the options array. This makes the Checkout form's select list appear blank in the Designer and default to USA in a browser.

Using Page Two for a Message

The checkout process works only if there are already items in the shopping cart. If the shopper attempts to open the Checkout form before adding items to the cart, the shopper needs to see an appropriate message. When no items have been found in a search, a separate Message form contains the message. When all items get deleted from the Cart form, the controls on the form morph into a message form. The Checkout form uses a third approach to messaging. Instead of using a separate form or changing the function of existing controls, this form uses a separate form page as a message form.

The Help form displays the Cart item count under the toolbar. It calculates the item count in an onServerLoad event. The Checkout form also displays summary information under the toolbar. In this case, the sum is the total price of all items rather than a simple item count.

You can create two methods to handle calculating the total price and handling an empty cart. The first method links to the form's onServerLoad event. The other moves the toolbar to page two. Here are the steps to create the two methods and add a message to page two.

  1. Open checkout.jfm in the Form Designer.
  2. Press the Page Down key to move to page two. The form will go blank because all controls currently reside on page one.
  3. Add a single HTML control below where the toolbar would appear. Use the properties from Table 14.8.

Table 14.8. Properties for the htmlEmpty control.

PropertyValue
namehtmlEmpty
height2
left1
top3.5
width65
text<H3>Your shopping cart is empty.</H3>

The position of the htmlEmpty control shown in Figure 14.7 is identical to the htmlMessage control in the Message form. This provides a consistent look and feel between forms. After this page has the toolbar, it will look just like the standard Message form.
Figure 14.7 : Adding htmlEmpty to page two.

  1. Create a new unlinked method to move the toolbar to page one. From the menu, select Method|New Method. Remove the {Export} comment, rename the method, and enter the following code.
    function MoveToolbar( thisForm, nPage )
    {
    // first 7 elements are the toolbar
    for (var i = 0 ; i < 8 ; i++ ) {
    thisForm.elements[i].pageno = nPage ;
    }
    }
    This method takes advantage of the elements array of a form. The elements array references all the controls contained in a form. It does not include database objects. You can use the elements array to modify controls without knowing the control names. In this case, the method uses elements to move the first seven controls to a given page.
  2. Use the Method Editor to modify the Form_onServerLoad method. Enter the code shown in Listing 14.1. When you've finished, move back to page one and save the form.


Listing 14.1. The code for checkout::Form_onServerLoad().

 1: function Form_onServerLoad()2: {

 2:    var cartRow = this.cart1.rowset,

 3:        titleRow = this.title1.rowset,

 4:        customerRow = this.customer1.rowset,

 5:        nTotal = 0 ;

 6:    this.buttonCheckout.visible = false ;

 7:

 8:    try {

 9:       var x = this.user ;  // Is user key undefined?

10:    }

11:    catch (Exception error) {

12:       if (error.code == 167) {                

13:          this.user = "" + (new NetInfo()).sessionID

14:       }

15:    }

16:

17:    cartRow.filter = "CARTUSER = " + "'" + this.user + "'" ;

18:    if ( cartRow.count() == 0 ) {

19:       this.pageno = 2 ;

20:       class::MoveToolbar( this, 2 ) ;

21:    }

22:    else {

23:       this.pageno = 1 ;

24:       cartRow.first() ;

25:       while ( ! cartRow.endOfSet ) {

26:          nTotal += ( cartRow.fields["QTY"].value *

27:             titleRow.fields["PRICE"].value ) ;

28:          cartRow.next() ;

29:       }

30:       this.htmlTotal.text = "<H4>Total Order $" + nTotal + "</H4>" ;

31:       customerRow.beginAppend() ;

32:    }

33: }


The Form_onServerLoad() method serves two functions. First it determines whether a shopper is ready to check out. If so, it creates a buffer in the Customer table. Otherwise, the function switches over to page two.

Like most methods that deal with queries, Listing 14.1 begins by creating shortcut references to the rowset objects. Lines 2 through 5 also create a temporary variable to use in calculating the total price.


function Form_onServerLoad()

{

   var cartRow = this.cart1.rowset,

       titleRow = this.title1.rowset,

       customerRow = this.customer1.rowset,

       nTotal = 0 ;

Lines 8 through 15 check to see whether the user property exists. This code is a shortened version of the user key generation routine in the Help form. This code lets you run the Checkout form directly from the IntraBuilder Explorer or a browser. If you are not running from a browser, line 13 creates the user property as an empty string.


try {

   var x = this.user ;  // Is user key undefined?

}

catch (Exception error) {

   if (error.code == 167) {

      this.user = "" + (new NetInfo()).sessionID

    }

}

The filter expression on line 17 is identical to the one in the Cart form and the Help form. You might want to comment out the filter assignment if you want to test run the Checkout form with all the data in the Cart table. If no rows are found, the form switches to page two, and the toolbar also moves.

Changing the pageno property of the form changes what page the user sees. Changing the pageno property of a control changes the control's page association. A control is not visible unless it is has a pageno equal to the form. The exception to this rule is if a control is on a pageno of zero. Controls on page zero appear on all pages. Normally, the form is set only to page zero at design-time.


cartRow.filter = "CARTUSER = " + "'" + this.user + "'" ;

if ( cartRow.count() == 0 ) {

   this.pageno = 2 ;

   class::MoveToolbar( this, 2 ) ;

}

If there are rows, line 23 sets the pageno to 1. Although 1 is normally the default, it is a good idea to explicitly set pageno in the onServerLoad whenever you are working with multiple page forms. By always setting the pageno property in the onServerLoad, the form will work, even if you forget to return to page one when you make changes in the Form Designer.


else {

   this.pageno = 1 ;

A while loop is used to calculate the total price of goods in the cart. Lines 26 and 27 comprise a single summation statement. The loop works only if the masterRowset property of the Title query points to the rowset of the Cart query. If the relation is not set correctly, no syntax error occurs, but the resulting nTotal value is incorrect.


cartRow.first() ;

while ( ! cartRow.endOfSet ) {

   nTotal += ( cartRow.fields["QTY"].value *

      titleRow.fields["PRICE"].value ) ;

   cartRow.next() ;

}

Line 30 displays the calculated field in a level-three heading, and line 31 creates an empty row buffer. The value in nTotal defaults to showing two decimal places, which is exactly what you need here. The Rowset::beginAppend() method creates an uncommitted buffer for a new row. Here, the new row is datalinked to several Text controls. All of them will appear blank and allow data entry. New rows cannot be committed without an explicit call to Rowset::Save().


this.htmlTotal.text = "<H4>Total Order $" + nTotal + "</H4>" ;

customerRow.beginAppend() ;

If you run the form in the Designer and have nothing in the cart, page one flashes on the screen before the message on page two. It doesn't look very good in the Designer, but this flash does not carry over into the browsers. Shoppers will see only the appropriate page with the toolbar, as shown in Figure 14.8.

Figure 14.8 : Viewing page two of the Checkout form in Navigator.

Validating Data with a Pseudo-Modal Dialog Box

Most Windows applications contain both modal and modeless windows. Common uses of modal dialog boxes are property sheets, error messages, and prompting for a filename. Modeless windows can be SDI (Single Document Interface) windows, such as Notepad, or MDI (Multiple Document Interface) windows, such as you find in Word, Excel, and IntraBuilder. Windows development environments make modal window creating easy through methods, such as Delphi's ShowModal() and Visual dBASE's ReadModal().

Modal dialogs are windows that the user must respond to before continuing to work with an application. They usually contain buttons such as OK and Cancel that close the dialog. As a general rule, users cannot resize a modal dialog. HTML does not have any direct support for modal dialogs.

HTML forms are by nature non-modal. You can add some simple dialog boxes, such as alert() and confirm(), through client-side JavaScript. For server-side events, you can mimic the behavior of a modal dialog box by presenting an HTML form with limited links. These forms cannot prevent the user from jumping out of your application completely, but they can control the flow of forms within an application.

For the Checkout form, shoppers must fill in every field before the order can be processed. Although much more extensive validation is possible, including direct validation of credit card information with a credit card agency, this example checks only that fields are not blank. If one or more blank fields is found, the user needs notification and a way to correct the error. This is where the pseudo-modal form comes in. It can list any blank fields and provide a single button that returns the shopper to page one of the Checkout form. A good way to implement a pseudo-modal form is through form-paging. Follow these steps to set up page three as the pseudo-modal form.

  1. Open the Checkout form in the Form Designer and switch to page three. You can press Page Up and Page Down to change pages. The current page number appears in the status line.
  2. Add three HTML controls with the properties listed in Table 14.9. Notice that you are not leaving room for the toolbar. As a modal type form, this form will have only one point of departure rather than the array of options provided by the toolbar.

Table 14.9. Position properties for the error message controls.

name
height
left
top
width
errorPrompt
1
1
1
65
errorList
8
1
2.5
65
errorHelp
1
1
11
65

  1. Change the text property for errorPrompt to the following:
    Unable to process order. The following fields cannot be left blank:
  2. Leave the errorList with a default text property and change the text property for errorHelp to the following:
    Press Continue to return to the Checkout form and correct the problem.
  3. Add a button with the properties shown in Table 14.10. This is the last form control for the shopping cart application.

Table 14.10. Properties for the buttonContinue control.

PropertyValue
namebuttonContinue
left1
top13
width12
textContinue

  1. Create and link a new method to the onServerClick event of the new button. Edit the buttonContinue_onServerClick() method as shown in the following code.
    function buttonContinue_onServerClick()
    {
    this.form.title = "Boingo's Books - Checkout" ;
    this.form.pageno = 1 ;
    }
    This button restores the title for page one and changes the page to page one. To the shopper, it will appear as though the Checkout form has reopened. The form maintains all previously entered data.
  2. At this point, page three should resemble Figure 14.9. You are done with that page. Switch back to page one and select the Buy Now button.
    Figure 14.9 : Designing page three of the Checkout form.

  3. Create and link a new method to the Cancel button's onServerClick event. Enter the following method:
    function buttonCancel_onServerClick()
    {
    var nextForm = new messageForm() ;
    nextForm.htmlMessage.text = "Order canceled." ;
    nextForm.user = this.form.user ;
    nextForm.open() ;
    form.release() ;
    }
    Nothing really happens when a shopper cancels an order. The items remain in the cart, and the shopper moves to the Message form. Clicking any of the toolbar buttons has a similar effect. The Cancel button is simply a physiological aid for shoppers who expect to see a Cancel button.
  4. Create and link a new method to the Buy Now button's onServerClick event. Enter the following method:
    function buttonBuy_onServerClick()
    {
    if ( class::Blanks( this.form ) ) {
    this.form.title = "Boingo's Books - Alert" ;
    this.form.pageno = 3 ;
    }
    }
    The buttonBuy is the counterpart of buttonContinue. If the Blanks() method returns false, the two buttons toggle the pages back and forth.
  5. Create a new unlinked method. From the menu, select Method|New Method. Remove the {Export} comment and modify Listing 14.2. When you've finished, save the form and close the Designer.


Listing 14.2. The Blanks() validation method.

 1: function Blanks( thisForm )

 2: {

 3:    var lReturn = false,

 4:        textName = "",

 5:        fieldList = "" ;

 6:

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

 8:       if ( thisForm.elements[i].className == "Text" ) {

 9:          if ( thisForm.elements[i].value.length == 0 ) {

10:             lReturn = true ;

11:             textName = thisForm.elements[i].name ;

12:             fieldList += ("<LI>" + textName + "</LI>") ;

13:          }

14:       }

15:    }

16:

17:    thisForm.errorList.text = "<UL>" + fieldList + "</UL>" ;

18:    return ( lReturn ) ;

19: }


This function checks to see whether any of the Text controls have a blank value. You could expand this function to include complex validation, such as checking whether the credit card expiration date has passed and that the postal code is formatted correctly for the selected country.

The method takes the thisForm parameter to allow calling from any event. In this case, the form will be called only from the button. You also could leave the parameter out and replace all instances of thisForm with this.form. Leaving in the parameter makes the method more flexible for future enhancements to the form. Lines 3 through 5 define three local variables with the var statement. The lReturn variable is a flag that is true if blanks are found. The function starts with the assumption that no blanks will be found. The other two variables are for building an HTML bulleted list of blank fields.


function Blanks( thisForm )

{

   var lReturn = false,

       textName = "",

       fieldList = ""

The for loop goes through all the controls on the form. The if on line 8 checks to see whether any of the controls are Text controls. No other control type requires validation.


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

   if ( thisForm.elements[i].className == "Text" ) {

If a Text control is found, line 9 checks the length of the value to see whether the field is blank. The value property is always trimmed of trailing spaces. Any field that contains only spaces has a length of zero. Any blanks cause the lReturn flag to become true. If all you needed the method to do was find blanks, you could include a break statement to break out of the loop when the first blank was found.

The method does not break out of the loop, so it can build a bulleted list of all blank fields. The <LI> and </LI> HTML tags delimit the items in a list. These tags work for both numbered lists and bulleted lists.


if ( thisForm.elements[i].value.length == 0 ) {

   lReturn = true ;

   textName = thisForm.elements[i].name ;

   fieldList += ("<LI>" + textName + "</LI>") ;

}

Creating the field list is simple because each Text control name is identical to the datalink field name. If control names were not the same as the field names, you could substitute dataLink.fieldName for name when generating the list.


textName = thisForm.elements[i].dataLink.fieldName ;

Line 17 encloses the field list with HTML unnumbered list tags. An unnumbered list is a bulleted list. Figure 14.10 shows the bulleted field list as it appears in Netscape Navigator. An HTML bulleted list of the U.S. flag's colors would look like this:


<UL><LI>Red</LI><LI>White</LI><LI>Blue<LI></UL>

Figure 14.10 : Page three of the Checkout form in Navigator.

The tag for an automatically numbered list is <OL>. If you replace the <UL> tags with <OL> tags, the list becomes a numbered list instead of a bulleted list.


   thisForm.errorList.text = "<UL>" + fieldList + "</UL>" ;

   return ( lReturn ) ;

}

Populating the Customer, Invoice, and Lineitem Tables

Up to this point, the Invoice and Lineitem tables have been dormant. In this section, you will learn how to write the methods that populate these tables and commit a new row to the Customer table. It is okay to commit changes now that the form has checked for an empty Cart and blank customer fields.

You can make all the remaining changes to the Checkout form through the Script Editor. Open the Checkout form in the Script Editor and modify buttonBuy_onServerClick() to call three new methods, as shown in Listing 14.3. Also add the three methods from Listings 14.4, 14.5, and 14.6.


Listing 14.3. The buttonBuy_onServerClick() method without a call to the Receipt report.

 1: function buttonBuy_onServerClick()

 2: {

 3:    if ( class::Blanks( this.form ) ) {

 4:       this.form.title = "Boingo's Books - Alert" ;

 5:       this.form.pageno = 3 ;

 6:    }

 7:    else {

 8:       class::SaveCustomer( this.form ) ;

 9:       class::SaveInvoice( this.form ) ;

10:       class::SaveLineitem( this.form ) ;

11:       this.form.release() ;

12:    }

13: }


The code to check for blanks remains the same. Lines 7 through 12 contain a new else block to call three methods and release the current form. Later, you can execute a report within this same block. However, to design the report, you first need some sample data. Running this block as it is will provide you with the data necessary to design the report.


else {

   class::SaveCustomer( this.form ) ;

   class::SaveInvoice( this.form ) ;

   class::SaveLineitem( this.form ) ;

   this.form.release() ;

}

This version of the buttonBuy_onServerClick() method is not designed to run in a browser. The form closes without creating new output for the browser. When run in a Designer, the form appears as an MDI window that closes normally. In a browser, the form occupies the browser display area without having its own window. A browser never expects a form to close without having something to take its place.


Listing 14.4. The SaveCustomer() method.

 1: function SaveCustomer( thisForm )

 2: {

 3:    var customer2 = new Query("select * from customer") ;

 4:    var customer1Row = thisForm.customer1.rowset,

 5:        customer2Row = customer2.rowset ;

 6:

 7:    if ( thisForm.radioVisa.value ) {

 8:       customer1Row.fields["CARD"].value = "VISA" ;

 9:    }

10:    else if( thisForm.radioMC.value ) {

11:       customer1Row.fields["CARD"].value = "MC" ;

12:    }

13:    else {

14:       customer1Row.fields["CARD"].value = "AMEX" ;

15:    }

16:

17:    customer2Row.indexName = "CUSTOMER" ;

18:    customer2Row.last() ;

19:    customer1Row.fields["CUSTOMER"].value = 

20:     customer2Row.endOfSet ? 1 : customer2Row.fields["CUSTOMER"].value+1;

21:    customer1Row.save() ;

22:    customer2.active = false ;

23: }


The SaveCustomer() method commits the datalinks in the row buffer along with the credit card type and a customer number. The credit card type is a simple code used in place of the longer descriptive text on the radio buttons. The codes VISA, MC, and AMEX translate to Visa, Master Card, and American Express, respectively. The customer number is a unique sequential number. To help in calculating the next available customer number, line 3 creates an additional query for the Customer table.

Line 3 passes the SQL command to the query constructor as a shortcut way to assign the sql property value and activate the query. Lines 4 and 5 contain a separate var statement that creates shortcut references to the rowset objects of the original and new customer queries.


var customer2 = new Query("select * from customer") ;

var customer1Row = thisForm.customer1.rowset,

    customer2Row = customer2.rowset ;

Lines 7 through 15 contain a series of if and else blocks to assign the correct credit card type based on the radio buttons. The value property of a radio button is true when the value is selected. Because all three have the same groupName property, only one can be selected at a time.


if ( thisForm.radioVisa.value ) {

   customer1Row.fields["CARD"].value = "VISA" ;

}

else if( thisForm.radioMC.value ) {

   customer1Row.fields["CARD"].value = "MC" ;

}

else {

   customer1Row.fields["CARD"].value = "AMEX" ;

}

An index helps quickly determine the highest customer number currently stored in the table. Line 18 moves the row pointer to the last row in the ascending index to locate the highest value. The next customer number is one higher than the current highest. If no rows are present in the table, the method uses one as the initial customer number.


customer2Row.indexName = "CUSTOMER" ;

customer2Row.last() ;

customer1Row.fields["CUSTOMER"].value = 

  customer2Row.endOfSet ? 1 : customer2Row.fields["CUSTOMER"].value + 1 ;

You also could use a single query to perform the operation. Using two queries cuts down on the interval between the time the next highest value is calculated and the time the new row is committed. The longer the interval, the higher the probability that a conflict will occur when two shoppers fire the routine simultaneously on separate remote agents. If this happens, there is a possibility of both shoppers getting the same customer number.

If you are working with remote agents on a high-volume site, you can ensure unique key values by placing a lock on the last row. In that case, you will need to create another error-handling page similar to page three. The new page could inform the user to try again in a few seconds and use the same Continue button to return to page one.


customer2Row.indexName = "CUSTOMER" ;

customer2Row.last() ;

if ( customer2Row.lockRow() ) {

   customer1Row.fields["CUSTOMER"].value = 

    customer2Row.endOfSet ? 1 : customer2Row.fields["CUSTOMER"].value + 1 ;

   customer1Row.save() ;

}

else {

   this.form.pageno = 4 ; // New page with "please wait" message.

}

Line 21 commits the new values and completes the append operation that began in the onServerLoad event. Line 21 deactivates the temporary query to recover resources no longer needed by the form.


customer1Row.save() ;

customer2.active = false ;

Another way to ensure unique customer numbers is to use Paradox tables in place of dBASE tables. Although Paradox tables are slower to work with, they do have an auto-increment field type that automatically does what this routine does for the customer number field.


Listing 14.5. The SaveInvoice() method.

 1: function SaveInvoice( thisForm )

 2: {

 3:    var invoice2 = new Query("select * from invoice") ;

 4:    var customerRow = thisForm.customer1.rowset

 5:        invoice1Row = thisForm.invoice1.rowset,

 6:        invoice2Row = invoice2.rowset ;

 7:

 8:    invoice1Row.beginAppend() ;

 9:    invoice1Row.fields["CUSTOMER"].value =

10:       customerRow.fields["CUSTOMER"].value ;

11:    invoice1Row.fields["ORDERDATE"].value = (new Date()) ;

12:    invoice2Row.indexName = "INVOICE" ;

13:    invoice2Row.last() ;

14:    invoice1Row.fields["INVOICE"].value = 

15:     invoice2Row.endOfSet ? 1 : invoice2Row.fields["INVOICE"].value+1;

16:

17:    invoice1Row.save() ;

18:    invoice2.active = false ;

19: }


The SaveInvoice() method uses the same logic as the SaveCustomer() method to increment the key value. It also places the incremental code right before the Rowset::save() to immediately commit the new key.

The new code starts on line 8 where a Rowset::beginAppend() creates a new row buffer. This was not necessary in the previous method because a buffer already existed on the Customer table. The customer number is set up as a foreign key that relates the Invoice table to the Customer table. Line 11 sets the ORDERDATE field value to a new Date object without creating a reference for the Date object. The (new Date()) expression creates a Date object that returns the current date and disappears.


invoice1Row.beginAppend() ;

invoice1Row.fields["CUSTOMER"].value =

   customerRow.fields["CUSTOMER"].value ;

invoice1Row.fields["ORDERDATE"].value = (new Date()) ;

The Results form uses a Date object in a similar manner when updating the Cart table. Each table type stores dates in a slightly different manner. IntraBuilder automatically translates the Date object into a format understood by the current table.


Listing 14.6. The SaveLineitems() method.

 1: function SaveLineitem( thisForm )

 2: {

 3:    var invoiceRow = thisForm.invoice1.rowset,

 4:        lineitemRow = thisForm.lineitem1.rowset,

 5:        cartRow = thisForm.cart1.rowset ;

 6:    cartRow.first() ;

 7:

 8:    while ( ! cartRow.endOfSet ) {

 9:       lineitemRow.beginAppend() ;

10:       lineitemRow.fields["INVOICE"].value =

11:          invoiceRow.fields["INVOICE"].value ;

12:       lineitemRow.fields["ISBN"].value = cartRow.fields["ISBN"].value;

13:       lineitemRow.fields["QTY"].value = cartRow.fields["QTY"].value;

14:       lineitemRow.save() ;

15:       cartRow.delete() ;

16:    }

17: }


The Lineitem table does not contain an incremental key. The main key value of the Lineitem table is a foreign key linking it to the Invoice table. The SaveLineitem() method assigns that key to each new row that it moves from the Cart table to the Lineitem table.

Like the other save methods, this one starts by creating shortcut references to the rowsets. It does not create any temporary query objects. Line 7 calls Rowset::first() to make sure all rows in the Cart table get processed. Without this call, the method would do nothing because the row pointer was left at endOfSet after calculating the total price in the onServerLoad event.


var invoiceRow = thisForm.invoice1.rowset,

    lineitemRow = thisForm.lineitem1.rowset,

    cartRow = thisForm.cart1.rowset ;

cartRow.first() ;

The while loop moves through each row using Rowset::delete() in place of Rowset::next(). Line 15 deletes each row causing the row pointer to move to the same place as a Rowset::next() would. Typically, you would place a call to Rowset::next() at the bottom of a loop. For each row in the Cart table, a new row is created and committed into the Lineitem table.


while ( ! cartRow.endOfSet ) {

   lineitemRow.beginAppend() ;

   lineitemRow.fields["INVOICE"].value =

     invoiceRow.fields["INVOICE"].value ;

   lineitemRow.fields["ISBN"].value = cartRow.fields["ISBN"].value ;

   lineitemRow.fields["QTY"].value = cartRow.fields["QTY"].value ;

   lineitemRow.save() ;

   cartRow.delete() ;

}

The user key and cart date are not transferred into the Lineitem table. Equivalent values can be found in the related Customer and Invoice tables. The customer number in the Customer table replaces the functionality of the user key. The order date in the Lineitem table replaces the cart date.

After modifying the buttonBuy_onServerClick() and creating the three save methods, run the form in the Designer, add a few books into the cart, and make a purchase. This will set up some valid sample data for creating a Receipt report.

After you have some sample data, you can create an empty report and complete the Checkout form. Use the following steps to create an empty report and call it from the Checkout form.

  1. Open the Script Pad and enter the following JavaScript statement:
    _sys.reports.design("receipt.jrp") ;
  2. Press Ctrl+S to save the blank report. If you do not explicitly save the form, closing a blank report will abandon it without any prompting.
  3. Close the Report Designer.
  4. Open the Checkout form in the Script Editor and add the following JavaScript statement to the buttonBuy_onServerClick() method, as shown on lines 423 and 424 of Listing 14.7.
    _sys.scripts.run("RECEIPT.JRP", 1, -1,
    this.form.invoice1.rowset.fields["INVOICE"].value) ;

You can now try the Checkout form through a browser. However, the blank report translates into a blank HTML page. In the next section, you will learn how to design a receipt with the Report Designer. If things are not going as expected, refer to Listing 14.7, which shows the complete source code for the Checkout form.


Listing 14.7. The completed Checkout form.

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

  2: // Generated on 01/02/97

  3: //

  4: var f = new checkoutForm();

  5: f.open();

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

  7:    with (this) {

  8:       onServerLoad = class::Form_onServerLoad;

  9:       height = 15;

 10:       left = 0;

 11:       top = 0;

 12:       width = 75;

 13:       title = "Boingo's Books - Checkout";

 14:    }

 15: 

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

 17:       left = 70;

 18:       top = 2;

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

 20:       active = true;

 21:    }

 22: 

 23:    with (this.invoice1.rowset) {

 24:    }

 25: 

 26:    with (this.customer1 = new Query()){

 27:       left = 70;

 28:       top = 1;

 29:       sql = 'SELECT * FROM "customer.DBF"';

 30:       active = true;

 31:    }

 32: 

 33:    with (this.customer1.rowset) {

 34:    }

 35: 

 36:    with (this.lineitem1 = new Query()){

 37:       left = 70;

 38:       top = 3;

 39:       sql = 'SELECT * FROM "lineitem.DBF"';

 40:       active = true;

 41:    }

 42: 

 43:    with (this.lineitem1.rowset) {

 44:    }

 45: 

 46:    with (this.cart1 = new Query()){

 47:       left = 70;

 48:       top = 4;

 49:       sql = 'SELECT * FROM "cart.DBF"';

 50:       active = true;

 51:    }

 52: 

 53:    with (this.cart1.rowset) {

 54:    }

 55: 

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

 57:       left = 70;

 58:      top = 5;

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

 60:      active = true;

 61:    }

 62: 

 63:    with (this.title1.rowset) {

 64:       indexName = "ISBN";

 65:       masterRowset = parent.parent.cart1.rowset;

 66:       masterFields = "ISBN";

 67:    }

 68: 

 69:    with (this.headCustomer = new HTML(this)){

 70:       height = 1;

 71:       left = 1;

 72:       top = 4.5;

 73:       width = 10;

 74:       color = "black";

 75:       text = "<H4>Customer</H4>";

 76:    }

 77: 

 78:    with (this.headAddress = new HTML(this)){

 79:       height = 1;

 80:       left = 1;

 81:       top = 6.5;

 82:       width = 10;

 83:       color = "black";

 84:       text = "<H4>Address</H4>";

 85:    }

 86: 

 87:    with (this.headCard = new HTML(this)){

 88:       height = 1;

 89:       left = 1;

 90:       top = 9.5;

 91:       width = 10;

 92:       color = "black";

 93:       text = "<H4>Credit Card</H4>";

 94:    }

 95: 

 96:    with (this.labelName = new HTML(this)){

 97:       height = 1;

 98:       left = 12;

 99:       top = 4.5;

100:       width = 11.5;

101:      color = "black";

102:      text = "Name";

103:    }

104: 

105:    with (this.labelPhone = new HTML(this)){

106:       height = 1;

107:       left = 12;

108:       top = 5.5;

109:       width = 11.5;

110:       color = "black";

111:       text = "Phone";

112:    }

113: 

114:    with (this.labelStreet = new HTML(this)){

115:       height = 1;

116:       left = 12;

117:       top = 6.5;

118:       width = 11.5;

119:       color = "black";

120:       text = "Street";

121:    }

122: 

123:    with (this.labelCity = new HTML(this)){

124:       height = 1;

125:       left = 12;

126:       top = 7.5;

127:       width = 11.5;

128:       color = "black";

129:       text = "City";

130:    }

131: 

132:    with (this.labelState = new HTML(this)){

133:       height = 1;

134:       left = 60;

135:       top = 7.5;

136:       width = 5;

137:       color = "black";

138:       text = "State";

139:    }

140: 

141:    with (this.labelPostal = new HTML(this)){

142:       height = 1;

143:       left = 12;

144:       top = 8.5;

145:       width = 11.5;

146:       color = "black";

147:       text = "Postal Code";

148:    }

149:

150:    with (this.labelCNumber = new HTML(this)){

151:       height = 1;

152:       left = 12;

153:       top = 10.5;

154:       width = 11.5;

155:       color = "black";

156:       text = "Cart #";

157:    }

158: 

159:    with (this.labelCName = new HTML(this)){

160:       height = 1;

161:       left = 12;

162:       top = 11.5;

163:       width = 11.5;

164:       color = "black";

165:       text = "Name on Card";

166:    }

167: 

168:    with (this.labelCDate = new HTML(this)){

169:       height = 1;

170:       left = 12;

171:       top = 12.5;

172:       width = 11.5;

173:       color = "black";

174:       text = "Exp. Date";

175:    }

176: 

177:    with (this.name = new Text(this)){

178:       left = 29;

179:       top = 4.5;

180:       width = 40;

181:       dataLink = parent.customer1.rowset.fields["NAME"];

182:    }

183: 

184:    with (this.phone = new Text(this)){

185:       left = 29;

186:       top = 5.5;

187:       width = 20;

188:       dataLink = parent.customer1.rowset.fields["PHONE"];

189:    }

190: 

191:    with (this.street = new Text(this)){

192:       left = 29;

193:       top = 6.5;

194:       width = 40;

195:       dataLink = parent.customer1.rowset.fields["STREET"];

196:    }

197: 

198:    with (this.city = new Text(this)){

199:       left = 29;

200:       top = 7.5;

201:       width = 30;

202:       dataLink = parent.customer1.rowset.fields["CITY"];

203:    }

204: 

205:    with (this.state = new Text(this)){

206:       left = 66;

207:       top = 7.5;

208:       width = 3;

209:       dataLink = parent.customer1.rowset.fields["STATE"];

210:    }

211: 

212:    with (this.postalcode = new Text(this)){

213:       left = 29;

214:       top = 8.5;

215:       width = 12;

216:       dataLink = parent.customer1.rowset.fields["POSTALCODE"];

217:    }

218: 

219:    with (this.cardnumber = new Text(this)){

220:       left = 29;

221:       top = 10.5;

222:       width = 20;

223:       dataLink = parent.customer1.rowset.fields["CARDNUMBER"];

224:    }

225: 

226:    with (this.cardname = new Text(this)){

227:       left = 29;

228:       top = 11.5;

229:       width = 40;

230:       dataLink = parent.customer1.rowset.fields["CARDNAME"];

231:    }

232: 

233:    with (this.carddate = new Text(this)){

234:       left = 29;

235:       top = 12.5;

236:       width = 12;

237:       dataLink = parent.customer1.rowset.fields["CARDDATE"];

238:    }

239: 

240:    with (this.htmlTotal = new HTML(this)){

241:       height = 1;

242:       left = 1;

243:       top = 3.5;

244:       width = 65;

245:       color = "black";

246:       text = "HTML1";

247:    }

248: 

249:    with (this.radioVisa = new Radio(this)){

250:       height = 1;

251:       left = 12;

252:       top = 9.5;

253:       width = 11.5;

254:       text = "Visa";

255:       value = true;

256:       groupName = "card";

257:    }

258: 

259:    with (this.radioMC = new Radio(this)){

260:       height = 1;

261:       left = 29;

262:       top = 9.5;

263:       width = 12;

264:       text = "Master Card";

265:       value = false;

266:       groupName = "card";

267:    }

268: 

269:    with (this.radioAMEX = new Radio(this)){

270:       height = 1;

271:       left = 46;

272:       top = 9.5;

273:       width = 23;

274:       text = "American Express";

275:       value = false;

276:       groupName = "card";

277:    }

278: 

279:    with (this.selectCountry = new Select(this)){

280:       left = 46;

281:       top = 8.5;

282:       width = 23;

283:       dataLink = parent.customer1.rowset.fields["COUNTRY"];

284:       options = 'array {"USA","AUSTRALIA","FRANCE","GERMANY",'+ 

285:                 '"HUNGARY","INDONESIA","IRELAND","JAPAN",""}';

286:    }

287: 

288:    with (this.buttonBuy = new Button(this)){

289:       onServerClick = class::buttonBuy_onServerClick;

290:       left = 1;

291:       top = 13.5;

292:       width = 12;

293:       text = "Buy Now";

294:    }

295: 

296:    with (this.buttonCancel = new Button(this)){

297:       onServerClick = class::buttonCancel_onServerClick;

298:       left = 15;

299:       top = 13.5;

300:       width = 12;

301:       text = "Cancel";

302:    }

303: 

304:    with (this.htmlEmpty = new HTML(this)){

305:       height = 2;

306:       left = 1;

307:       top = 3.5;

308:       width = 65;

309:       color = "black";

310:       text = "<H3>Your shopping cart is empty.</H3>";

311:       pageno = 2;

312:    }

313: 

314:    with (this.errorPrompt = new HTML(this)){

315:       height = 1;

316:       left = 1;

317:       top = 1;

318:       width = 65;

319:       color = "black";

320:       text = "Unable to process order. The following fields " +

321:              "cannot be left blank:";

322:       pageno = 3;

323:    }

324: 

325:    with (this.errorList = new HTML(this)){

326:       height = 8;

327:       left = 1;

328:       top = 2.5;

329:       width = 65;

330:       color = "black";

331:       text = "HTML1";

332:       pageno = 3;

333:    }

334: 

335:    with (this.errorHelp = new HTML(this)){

336:       height = 1;

337:       left = 1;

338:       top = 11;

339:       width = 65;

340:       color = "black";

341:       text = "Press Continue to return to the Checkout "+ 

342:              "form and correct the problem.";

343:       pageno = 3;

344:    }

345: 

346:    with (this.buttonContinue = new Button(this)){

347:       onServerClick = class::buttonContinue_onServerClick;

348:       left = 1;

349:       top = 13;

350:       width = 12;

351:       text = "Continue";

352:       pageno = 3;

353:    }

354: 

355:    function Form_onServerLoad()

356:    {

357:       var cartRow = this.cart1.rowset,

358:           titleRow = this.title1.rowset,

359:           customerRow = this.customer1.rowset,

360:           nTotal = 0 ;

361:       this.buttonCheckout.visible = false ;

362: 

363:       try {

364:          var x = this.user ;  // Is user key undefined?

365:       }

366:       catch (Exception error) {

367:          if (error.code == 167) {

368:             this.user = "" + (new NetInfo()).sessionID

369:          }

370:       }

371: 

372:       cartRow.filter = "CARTUSER = " + "'" + this.user + "'" ;

373:       if ( cartRow.count() == 0 ) {

374:          this.pageno = 2 ;

375:          class::MoveToolbar( this, 2 ) ;

376:       }

377:       else {

378:          this.pageno = 1 ;

379:          cartRow.first() ;

380:          while ( ! cartRow.endOfSet ) {

381:             nTotal += ( cartRow.fields["QTY"].value *

382:                titleRow.fields["PRICE"].value ) ;

383:             cartRow.next() ;

384:          }

385:          this.htmlTotal.text = "<H4>Total Order $"+nTotal+"</H4>";

386:          customerRow.beginAppend() ;

387:       }

388:    }

389: 

390:    function MoveToolbar( thisForm, nPage )

391:    {

392:       // first 7 elements are the toolbar

393:       for (var i = 0 ; i < 8 ; i++ ) {

394:          thisForm.elements[i].pageno = nPage ;

395:       }

396:    }

397: 

398:    function buttonContinue_onServerClick()

399:    {

400:       this.form.title = "Boingo's Books - Checkout" ;

401:       this.form.pageno = 1 ;

402:    }

403: 

404:    function buttonCancel_onServerClick()

405:    {

406:       var nextForm = new messageForm() ;

407:       nextForm.htmlMessage.text = "Order canceled." ;

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

409:       nextForm.open() ;

410:       form.release() ;

411:    }

412: 

413:    function buttonBuy_onServerClick()

414:    {

415:       if ( class::Blanks( this.form ) ) {

416:          this.form.title = "Boingo's Books - Alert" ;

417:          this.form.pageno = 3 ;

418:       }

419:       else {

420:          class::SaveCustomer( this.form ) ;

421:          class::SaveInvoice( this.form ) ;

422:          class::SaveLineitem( this.form ) ;

423:          _sys.scripts.run("RECEIPT.JRP", 1, -1,

424:             this.form.invoice1.rowset.fields["INVOICE"].value);

425:          this.form.release() ;

426:       }

427:    }

428: 

429:    function Blanks( thisForm )

430:    {

431:       var lReturn = false,

432:           textName = "",

433:           fieldList = "" ;

434:    

435:       for ( var i = 0 ; i < thisForm.elements.length ; i++ ){

436:          if ( thisForm.elements[i].className == "Text" ) {

437:             if ( thisForm.elements[i].value.length == 0 ) {

438:                lReturn = true ;

439:                textName = thisForm.elements[i].name ;

440:                fieldList += ("<LI>" + textName + "</LI>") ;

441:             }

442:          }

443:       }

444:    

445:       thisForm.errorList.text = "<UL>" + fieldList + "</UL>" ;

446:       return ( lReturn ) ;

447:    }

448: 

449:    function SaveCustomer( thisForm )

450:    {

451:       var customer2 = new Query("select * from customer");

452:       var customer1Row = thisForm.customer1.rowset,

453:           customer2Row = customer2.rowset ;

454: 

455:       if ( thisForm.radioVisa.value ) {

456:          customer1Row.fields["CARD"].value = "VISA" ;

457:       }

458:       else if( thisForm.radioMC.value ) {

459:          customer1Row.fields["CARD"].value = "MC" ;

460:       }

461:       else {

462:          customer1Row.fields["CARD"].value = "AMEX" ;

463:       }

464:   

465:       customer2Row.indexName = "CUSTOMER" ;

466:       customer2Row.last() ;

467:       customer1Row.fields["CUSTOMER"].value = 

468:    customer2Row.endOfSet ? 1 : customer2Row.fields["CUSTOMER"].value+1;

469:       customer1Row.save() ;

470:       customer2.active = false ;

471:    }

472: 

473:    function SaveInvoice( thisForm )

474:    {

475:       var invoice2 = new Query("select * from invoice") ;

476:       var customerRow = thisForm.customer1.rowset

477:           invoice1Row = thisForm.invoice1.rowset,

478:           invoice2Row = invoice2.rowset ;

479:  

480:       invoice1Row.beginAppend() ;

481:       invoice1Row.fields["CUSTOMER"].value =

482:          customerRow.fields["CUSTOMER"].value ;

483:       invoice1Row.fields["ORDERDATE"].value = (new Date()) ;

484:       invoice2Row.indexName = "INVOICE" ;

485:       invoice2Row.last() ;

486:       invoice1Row.fields["INVOICE"].value = 

487:    invoice2Row.endOfSet ? 1 : invoice2Row.fields["INVOICE"].value+1;

488:  

489:       invoice1Row.save() ;

490:       invoice2.active = false ;

491:    }

492: 

493:    function SaveLineitem( thisForm )

494:    {

495:       var invoiceRow = thisForm.invoice1.rowset,

496:           lineitemRow = thisForm.lineitem1.rowset,

497:           cartRow = thisForm.cart1.rowset ;

498:       cartRow.first() ;

499: 

500:       while ( ! cartRow.endOfSet ) {

501:          lineitemRow.beginAppend() ;

502:          lineitemRow.fields["INVOICE"].value =

503:            invoiceRow.fields["INVOICE"].value ;

504:          lineitemRow.fields["ISBN"].value=cartRow.fields["ISBN"].value;

505:          lineitemRow.fields["QTY"].value = cartRow.fields["QTY"].value;

506:          lineitemRow.save() ;

507:          cartRow.delete() ;

508:       }

509:    }

510:

511: }


The Receipt Report

The information to present on the Receipt report is a combination of some customer information and a listing of items purchased. Customer information goes into the page heading. The items go into the detail band and result in a table that is similar in layout to the Cart form. Fortunately, creating multiple rows is much easier in a report. You do not have to do anything beyond dropping fields in the detail band to have them replicated for each row in the table.

The receipt requires data from four tables: Customer, Invoice, Lineitem, and Title. The Customer and Invoice tables are for the page header only. The driving table for the report is the Lineitem table. This must be the first table added to the report. The Title table provides title and pricing information.

Three calculations are involved in the report. The total is calculated as quantity multiplied by price. Grand totals can be calculated for the quantity and total purchase.

Despite the fact that reports offer little in the way of user interaction, their design can be more challenging than designing a form. The IntraBuilder Report Designer is somewhat paradoxical. It provides incredibly precise layout control for printing reports from the Designer. However, you are likely to use it much more often for designing reports that display in a browser where HTML provides only crude control over the positioning.

NOTE
The unit of measure in a report is known as a twip. Twips are much smaller than a standard pixel. There are 1,440 twips in an inch.

To make the task easier, first design the report without any totals or calculations. After all the relations and basic positioning are set, you can go back and add in the JavaScript expressions required to perform totaling.

Receipt Relations

This section guides you through the initial report layout in two phases. The first phase is to lay out the detail bands. After that, you will tackle the header area. The design of the Receipt report does not involve the Report Expert. You will start by adding and relating the four tables. Unlike the Checkout form, all the query tables in the Receipt report have a specified relation. Use the following steps for setting up the detail band.

  1. Open the blank Receipt report in the Report Designer.
    _sys.reports.design("receipt.jrp")
  2. Drop the Lineitem table into the top-left corner of the report. Placing this table first establishes the Lineitem rowset as the driving rowset for the stream source.
    When the report runs, it shows a detail band for each row in the Lineitem table. If you add Customer first, the detail bands correspond to the customer. For a Receipt report, only one customer is ever printed.

The stream source is the report object that determines what rowset has a one-to-one correspondence with the detail band. IntraBuilder is capable of running reports that contain more than one stream source. This allows for adjacent detail areas that come from independent views. The Report Designer does not have any visual tools for defining multiple stream sources.

  1. Now add the Customer, Invoice, and Title tables. As you did when adding tables to forms, remove the full path from the sql property of each of the four queries.
  2. Relate the Lineitem table to the Invoice table. Select the Lineitem query and inspect the rowset of the query. Use the masterRowset drop-down to select invoice1. After setting the masterRowset, you can use the masterFields drop-down to select the INVOICE. This automatically sets the indexName to INVOICE, as shown in Figure 14.11.
    Figure 14.11 : Relating the Lineitem table to the Invoice table.

  3. Select the customer1 query. The Customer table also relates to the Invoice table. Descend to the rowset object and again select invoice1 from the masterRowset drop-down. This time, the masterFields property needs to be set to CUSTOMER.
  4. To create the third and last relation, inspect the rowset of the title1 query. The Title table relates to the Lineitem table on the ISBN field. Use the drop-down to select lineitem1 for the masterRowset. Set masterFields to ISBN.
  5. Reduce the page margins. Inspect the page template by selecting form.PageTemplate1 from the Inspector's drop-down list box. This is the outermost of the two grid lines that appear in the report pane. It controls the report margins. Change all four margins from 1,080 to 360.
    This changes the margins from 0.75 inches to 0.25 inches. Large margins are not necessary when the output is targeted at a browser. Shrinking the margin gives you a larger work area in the Designer. If you plan to send the report to a laser printer, do not reduce the margin to less than 0.5 inches.

TIP
Open up the group pane to see how the different bands of the report are laid out. By default, the group pane is closed. You can click and drag the window pane divider from the left edge to open the group pane.

  1. Make room for the larger page header by altering the position properties of the stream frame. This is the innermost of the two grid lines. Select the stream frame and inspect form.PageTemplate1.StreamFrame1. Set the position properties, as shown in Table 14.11.
    These settings allow for 1.75 inches between the top margin and the start of the first detail band. The top of the stream frame is actually 2 inches from the top of the physical page.

Table 14.11. Position properties for form.PageTemplate1.StreamFrame1.

PropertyValue In Inches
height10080 7
left360 0.25
top2520 1.75
width8640 6

TIP
In the Report Designer, it is much easier to control positioning through the Inspector than by dragging items with the mouse. The lack of a snap-to-grid feature and the twip orientation can make attempts to reposition controls with the mouse almost unbearable. Save yourself the stress and use the Inspector.

  1. Open the Field Palette and drop the ISBN field from the lineitem1 query into the stream frame. Position the ISBN field at the left edge of the stream frame. This automatically creates a detail band for each row in the lineitem1 query.
  2. Add the Qty, Title, and Price fields into the detail band. Drop the Qty field from the lineitem1 query to the right of the ISBN field. Drop the Title field from the title1 query to the right of Qty. Drop the Price field to the right of the title.
    As you add each field, the Designer adds two HTML controls. The first contains the field title and the second displays the actual value. The titles are given generic names such as titleIsbn, titleQty, and so on. The field value control names match the field names.
  3. Adjust the title positions, as shown in Table 14.12. Also, change the text for ISBN to uppercase.

Table 14.12. Datalinked controls from the Field Palette.

nametext heightlefttop width
titleIsbn<H2>ISBN</H2> 3963602020 1440
titleQty<H2>Qty</H2> 39619802020 720
titleTitle<H2>Title</H2> 39628802020 2880
titlePrice<H2>Price</H2> 39659402020 1080

The positions allow for 1/8 inch or 180 twips between each column. Notice that the left property of the first field label matches the left property of the stream frame.

  1. Bring the field values into alignment with the titles by using the positions shown in Table 14.13. Here, each left property is offset by 720 due to the stream frame margin. The widths are identical to the titles. You can quickly align and resize each column by selecting each title and field and using the sizing and alignment options of the Layout menu.

Table 14.13. Detail band field positions.

Name
height
left
top
width
isbn
255
0
0
1440
qty
255
1620
0
720
title
255
2520
0
2880
price
255
5580
0
1080

At this point, the Receipt report should resemble the one shown in Figure 14.12. The detail area looks fairly acceptable, except for the Qty and Price figures appearing one line too low. This is caused by the fact that the default width of any numeric is wider than a half inch. For both values presented here, one half inch is more than enough space. You can use a template to override the default field width.
Figure 14.12 : Positioning fields in the detail band.

  1. Because Boingo sells only whole books, you can set the template property of the Qty field to 999. Also set the template property of the Price field to 999.99. This will move the numeric fields back to the top line.
  2. This is a good point to save the report and try running it through a browser. You can use the INTRASRV.ISV module to run reports the same way you run a form.
    http://localhost/svr/intrasrv.isv?store\receipt.jrp

If you run the report through Navigator, you will see each field in a separate box. In Figure 14.13, you can see each cell of the table that defines the detail area. Viewing the cells can be handy in determining layout problems.

Figure 14.13 : Viewing the detail band layout through Navigator.

The browser shows how nicely the fields line up. Notice that the Title field does not wrap, which happens in the Designer. In the next phase of report design, you will be adding customer and invoice information into the header area.

  1. Reopen the Receipt report in the Report Designer.
  2. Before adding any new fields, right justify the titles for the Qty and Price fields. This will place the titles above the already right-aligned fields.
    HTML controls contain separate properties for horizontal and vertical alignment. To right align the titles, set the alignHorizontal properties to 2.
  3. Add three HTML controls from the Component Palette to create three new titles in the page heading. When you add fields from the Field Palette into the page heading area, IntraBuilder does not include titles. Use the values from Table 14.14 to create each title.

Table 14.14. Titles for Invoice table fields.

nametext
height
left
top
width
titleInvoice<H4>Invoice #</H4>
255
360
900
1440
titleCustomer<H4>Customer #</H4>
255
360
1200
1440
titleDate<H4>Order Date</H4>
255
360
1500
1440

These titles all align above the ISBN column. A heading level four makes the titles slightly larger than the field values but not as large as the column field titles.

  1. Open the Field Palette and drop all three fields from the Invoice table next to the appropriate titles. You can fine-tune the position properties as listed in Table 14.15. To avoid a wrap-around of the numeric fields, add template values that match the defined field width of 10.

Table 14.15. Position properties for the Invoice table fields.

nameheight lefttopwidth template
invoice
255
1980
900
1080
9999999999
customer
255
1980
1200
1080
9999999999
orderdate
255
1980
1500
1080
(blank)

  1. Add the customer address fields from the Customer table. Use the positions listed in Table 14.16. These fields are self-explanatory and do not require any titles. Figure 14.14 shows how the fields align in the page heading.
    Figure 14.14 : Positioning fields in the page heading.

Table 14.16. Position properties for the Customer table fields.

name
height
left
top
width
name
255
3600
 900
2880
street
255
3600
1200
2880
city
255
3600
1500
2160
state
255
6120
1500
 360
postalcode
255
6700
1500
 720

  1. Save and run the report in the Designer.

That is about all you can do for the Receipt report without getting your hands a little dirty. In the next section, you will add a calculated field and two grand totals.

Using Events to Calculate and Summarize

To add summary information, you can take advantage of three events: onDesignLoad, preRender, and canRender. The onDesignLoad event fires when you open a report in the Report Designer. The preRender event fires before a report runs. It is very similar to the onServerLoad event of a form. Within a report, canRender fires before an HTML control is drawn. The canRender event also can prevent the control from being rendered or streamed into HTML. All three events are server-side events, as are all report-specific events.

Creating methods within a report is the same as creating a method for a form. The major difference is the added hierarchy levels that exist within a report. Most form events are events of a control. Controls are only one level removed from the container form. Within a report, there can be many levels between an HTML control and the report object.

NOTE
Methods linked to report events can use the shortcut reference form to refer to the report. There is not a shortcut reference called report. This allows you to share custom control methods between forms and reports.

There are two ways to add summary fields to a report: automatic and manual. The automatic way is to go through the Add Groups and Summaries dialog box. This dialog box contains tabs that duplicate two steps from the Report Expert. The manual way is to use events and add your own code blocks. You also can modify the code blocks from generated summary fields. For the Receipt report summaries, you will use both automatic and manual summary creation.

Along with adding the total and grand total calculations, there are still a couple of HTML controls to add to the page heading. The following are the steps to complete the basic Receipt report.

  1. Open the Receipt report in the Report Designer.
  2. Add a large HTML control to the top-left of the form to thank shoppers for their purchases. Use the property values from Table 14.17 to create a heading that is centered across the entire report width.

Table 14.17. Property values for the "thank you" message.

PropertyValue
namehtmlThanx
alignHorizontal1-Center
height400
left0
top0
width9000
text<H2>Boingo thanks you for your purchase!</H2>

  1. Turn the "thank you" message into a link to the index page. Open the Text Property Builder and enter http://localhost/store/index.htm in the URL tag, as shown in Figure 14.15. You will need to change the localhost portion to the appropriate server name upon deployment. With the entire existing text selected, click Add to turn the text into a link.
    Figure 14.15 : Creating a link from the Receipt report back to the index.htm page.

TIP
Avoid dead-end pages by always including at least one hyperlink in every report. Pages with no links can frustrate browser users.

  1. Add another HTML control below the "thank you" message. This text is to remind people to create a hard copy of the receipt. Use the values shown in Table 14.18.

Table 14.18. Property values for the print message.

PropertyValue
namehtmlPrint
alignHorizontal1-Center
height255
left0
top500
width9000
textPrint this receipt for your records.

  1. Add a sum for the QTY field using Expert assistance. From the menu, select Layout|Add Groups and Summaries. Click the Summaries tab, as shown in Figure 14.16. Select QTY from the Available Fields list and click OK.
    Figure 14.16 : Adding a summary through the dialog box.


    This creates a new, red, and italic HTML control with some descriptive text and a summary. The number is all you really want.

  2. Select the new red control and adjust the property values as listed in Table 14.19. This places the total under the Qty column and leaves room for another summary on the same line.

Table 14.19. Adjusted properties for the Qty grand total.

PropertyValue
namegrandQty
alignHorizontal2-Right
template99999
fontBoldtrue
fontItalicfalse
height255
left1620
top0
width720
colorblack

  1. Remove the "Sum of Qty" from the text property code block. The resulting code block should read as follows:
    {||this.parent.parent.agSum(
    {||this.parent.StreamSource1.rowset.fields["QTY"].value})}
    You must edit the code block from within the Inspector. The Text Property Builder works only with literal strings. If you are having trouble with syntax, cut and paste the code block to a Script Editor window and copy it back after verifying the syntax.
  2. From the Component Palette, drop an HTML control into the detail band to the right of the Price field. This creates an HTML control in the detail band and another in the page header.
  3. Modify the name and positions of the two new controls. Use the values listed in Table 14.20. The title is contained in the page template object. The value control is within the detail band object.

Table 14.20. Properties for calculated total controls.

Nametext
height
left
top
width
titleTotal<H2>Total</H2>
396
7200
2020
1080
calcTotalHTML1
255
6840
0
1080

  1. Right-align both controls by setting the alignHorizontal property of labelTotal and calcTotal to 2. Also set the template property of calcTotal to 99999.99.
  2. Add another HTML control to show the grand total of the Total field. Use the property values from Table 14.21 to set up the control to match the look of the Qty summary.

Table 14.21. Position properties for the grandTotal control.

PropertyValue
namegrandTotal
alignHorizontal2-Right
template999999.99
fontBoldtrue
height255
left6840
top0
width1080

All the layout work is done. Your report should now resemble the one shown in Figure 14.17. The only thing left is to turn those HTML1 values into real totals.
Figure 14.17 : All controls before adding methods to calculate totals.

  1. Start with the report's preRender event. To select the report object, open the Inspector and choose form from the top of the drop-down list. Remember that form refers to the main container for both forms and reports. From the Events tab, click the preRender tool button to open the Method Editor. Enter the method as follows:
    function Form_preRender()
    {
    this.lineGrand = 0 ;
    this.lineTotal = 0 ;
    if (RECEIPT.arguments.length == 3) {
    this.invoice1.rowset.filter = "INVOICE = " + RECEIPT.arguments[2];
    }
    }
    The method creates properties on the report object to store the values for the line-item total and grand total values. Additionally, it sets up a filter if three arguments are passed to the RECEIPT function. The RECEIPT function is the unnamed function that runs when you call the report with _sys.scripts.run(). This lets you run the report with no filter from the IntraBuilder Explorer. The filter selects the current invoice when called from the Checkout form.
  2. Now create a method for the canRender event of the calcTotal field. Enter the following method:
    function calcTotal_canRender()
    {
    this.form.lineTotal =
    ( this.form.lineitem1.rowset.fields["QTY"].value *
    this.form.title1.rowset.fields["PRICE"].value ) ;
    this.form.lineGrand += this.form.lineTotal ;
    return ( true ) ;
    }
    This method does a simple quantity times price calculation to figure the total of any line item. It also adds the total into the grand total. The method must return true for the control to appear on the report.
    If you try to save and run the report now, it will run just as before. However, you will encounter the error shown in Figure 14.18 if you try to reopen it in the Designer.
    Figure 14.18 : Error when designing the Receipt report without using onDesignLoad.

  3. To avoid errors when redesigning, link the following method to the report object's onDesignLoad method. This method replicates the initialization code found in Form_preRender().
    function Form_onDesignLoad()
    {
    this.lineGrand = 0 ;
    this.lineTotal = 0 ;
    }
  4. After adding the Form_onDesignLoad() method, you can move freely between Run and Design mode. Try the toggle now to make sure the methods work right and to initialize the lineGrand and lineTotal properties. Both properties need to exist before continuing with the report design.
  5. Enter { || this.form.lineTotal } as a code block for the text property of calcTotal. Use the type drop-down list to change the property from a character string to a code block, as shown in Figure 14.19. If you don't change the type, the report prints the code block as literal text.
    Figure 14.19 : Setting up a self-evaluating code block for the line item total.

  6. Enter { || this.form.lineGrand } as the code block for the grand total.

NOTE
As you modify self-evaluating code blocks in the Designer, the canRender event continues to fire. This leads to incorrect grand total values when designing. There is no such adverse effect on the values when the report appears in Run mode.

  1. Select form.PageTemplate1 in the Inspector and set the gridLineWidth property to 0. When you've finished, save the report and try it in Run mode.

You have completed the last step for the Receipt report and for the entire shopping cart application. If you run the report directly in the Designer, it will always show information for the first customer and invoice. Running the report from the Cart form shows the current customer and invoice. Figure 14.20 shows the totals as they appear in Internet Explorer.

Figure 14.20 : Running the report through Internet Explorer.

Summary

Today you completed a week-long journey into the land of shopping cart application development for the Web. Shoppers can now search for books, add books to the cart, modify the cart contents, and check out of the bookstore. The application has come full circle. The final destination is the Receipt report, which links back to the index.htm where the application begins.

During the last day of development, you worked with pages for the first time this week. You learned how pages appear as independent forms yet allow complete sharing of methods and values. Pages provide an easy way to mimic modal behavior within a browser.

In creating a report, you learned some of the difficulties of manual page design. By using the Inspector to set values, you could better align items and create even spacing between columns. The Receipt report also provided an example of using report events.

As the week ends, you should feel comfortable in creating simple applications that are based exclusively on client-side JavaScript events. You have learned all it takes to create applications that dynamically update and display data and are compatible with both Netscape Navigator and Internet Explorer. You are now ready for the advanced topics of week three.

Q&A

Q:The Checkout form has some logic in the SaveCustomer() method to figure out the correct value for the Card field. Could this also be done through a field canChange event if the field were datalinked to the radio buttons?
A:You can use the canChange event to directly update the field if you also link a method to Card field's beforeGetValue event. For an update form, this would be preferable. The method linked to canChange must translate the text of each datalinked radio button to the appropriate code. Here is a method you could use:

function Card_canChange(newValue)
{
if ( newValue == "Visa" ) {
this.value = "VISA" ;
}
else if ( newValue == "Master Card" ) {
this.value = "MC" ;
}
else {
this.value = "AMEX" ;
}
return false ;
}
With the Checkout form, there is never any existing data to worry about. The form appends only new rows. As such, the approach used is simpler. If you want to enhance the form to allow customers to modify their account information, you also will need to incorporate a beforeGetValue method similar to the following:
function Card_beforeGetValue()
{
var cReturn = "American Express" ;
if ( this.value == "VISA" ) {
cReturn = "Visa" ;
}
else if ( this.value == "MC" ) {
cReturn = "Master Card" ;
}
return cReturn ;
}

Q:The validation routine in the Checkout form checked only that the fields were not blank. Could this validation be done through client-side JavaScript?
A:Yes, simple validations can be made through client-side JavaScript. In fact, you could use the same logic because client-side JavaScript fully supports the elements array. If validation is not essential or you know that all users will have JavaScript-enabled browsers, it is a good way to offload some of the processing to the client. The danger in client-side validation is that some browsers do not support JavaScript and some users turn JavaScript off in browsers that do.
Q:The Customer table uses a character field for the card expiration date. Wouldn't it make more sense to use a Date field?
A:Most credit cards show the expiration date as only a month and a year. A character field is used so users can enter exactly what appears on the card. You could substitute a Date field if it did not have a direct datalink. A datalink to a Date field will reject any dates that do not contain the complete month, day, and year. You could write a routine to convert a given string into an appropriate date value. In this case, you could substitute 07/98 for 07/01/98.
Q:The elements array on a form is great for dealing with controls, but what if I need to work with an unknown set of other objects such as queries?
A:IntraBuilder provides an alternate form of the for loop to determine all the properties of any object. The interesting thing about using a for loop to go through properties is that the properties are returned as character strings rather than their actual values. You can use the eval() function to get a proper reference. The following method uses this technique to deactivate all queries in the current form.
Function buttonDeactivate_onServerClick()
{
var aProp = new Array() ;
for ( var iProp in this.form ) {
aProp.add( eval("this.form." + iProp) ) ;
}
for (var i = 0 ; i < aProp.length ; i++ ) {
try {
if ( aProp[i].className == "Query" ) {
aProp[i].active = false ;
}
}
catch (Exception e) {
if ( ! (e.code == 163) ) {
_sys.scriptOut.writeln('Error: ' + e.message) ;
_sys.scriptOut.writeln(' code: ' + e.code) ;
}
}
}
}

TIP
Extracting unknown property names is a good way to discover undocumented or hidden properties. A hidden property is one that does not show up in the Inspector. For more information on undocumented properties see Appendix B, "The Undocumented IntraBuilder."

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. What is the purpose of a form's page number zero?
  2. What HTML tags are required to create a bulleted list?
  3. What is the Paradox table field type that creates unique numeric key values for you?
  4. How can you set the sql property and activate a new query with a single assignment statement?
  5. What is the report equivalent of a form's onServerLoad event?
  6. When creating a report, what method can you use to sum field values in a self-evaluating code block?

Exercises

  1. The shopping cart application does not automatically delete rows from the Cart table if a shopper leaves the system without making a purchase. You can add automatic row deletion through the onServerUnload event of the Toolbar custom form class.
    This event fires whenever a form is explicitly closed through Form::release() or when a form times out on an IntraBuilder Agent. In the shopping cart application, you want to delete rows from the Cart table only if a form times out or when the shopper makes a purchase. Explicit calls to Form::release() occur when a shopper moves from one form to another.
    Add the following method to the Toolbar custom form class and add a custom timeout property to all forms in the application. Default the timeout property to true and set it to false prior to any call to Form::release().
    function Form_onServerUnload()
    {
    if ( this.timeout ) {
    var q = new Query('select * from "cart.dbf"') ;
    q.rowset.filter = "CARTUSER = " + "'" + form.user + "'" ;
    while (! endOfSet ) {
    q.rowset.delete() ;
    }
    q.active = false ;
    }
    }
  2. The current Checkout form requires shoppers to reenter address and credit card information for every purchase. The table structure has provisions for return customers. The Customer table contains a unique Customer number for all previous customers. As presented, the Checkout form creates a one-to-one correspondence between the Customer and Invoice tables. A business is more likely to succeed if the relation between Customer and Invoices is one-to-many.
    Change the Checkout form to accept an existing customer number for existing customers. You can do this by adding a new initial page with options for entering an existing Customer number or proceeding to the Customer data entry page.
  3. When you have completed all modifications to all the application files, you can include rather than load the files. Try this out by commenting out the #define DEBUG line in the store.h file.
    If you check the file size of help.jfo before and after commenting #define DEBUG, you will see a substantial difference. The compile dialog box also will show the number of executable lines going up from less than 200 lines to over 1,500 lines. The newer object file will contain all source code except the report. Leave the receipt.jrp out of the store.h to avoid problems with parameter passing.