Day 12

Creating a Dynamic Grid for the Cart Form

by Paul Mahar


CONTENTS

Today you will take the Cart form from Day 11 and expand it into a form that allows dynamic updates of multiple rows of the Cart table. The challenge of the day is to create a grid that displays and allows updates to more than the current row. IntraBuilder does not have any multiple row controls, such as the Delphi DBGrid or the Visual dBASE Browse control. For the Cart form, you will need to create a "roll your own" grid.

Creating your first dynamic live data grid can be a daunting task. To make it easy, you can break the process into the following tasks:

By the end of the day, you will know how to dynamically generate and manipulate controls from JavaScript methods. The ability to create controls at runtime lets you build data-driven forms that are difficult or impossible to represent through the visual tools of the Form Designer.

TIP
The three tasks are represented by three incarnations of the Cart form on the CD-ROM. They are available with the Day 12 source files as cart1.jfm, cart2.jfm, and cart3.jfm. You can use these files to skip over any of the tasks or as reference points if you get lost along the way.

The Single Item Starter Cart

The arrangement of the Cart form is similar to an order form. The main portion of the Cart form is a grid or table consisting of five columns. Two columns show values from the Cart table. Another two show values from the Title table. The last column is a calculated expression. The five columns contain the following values:

NOTE
To see an order form example that uses the same basic layout as the Cart form, turn to the back of this book. In the pages following the index, Macmillan includes an order form for books with topics similar to the one you're reading.

In addition to the columns, the Cart form contains several other new controls. When you move from the single item cart to using a grid, all the column controls move from visible controls in the Form Designer to JavaScript statements. The other controls are more static than the grid controls. The non-grid controls will display only once; however, grid controls can display more than once. The following controls will remain visible in the Form Designer throughout the three incarnations of the Cart form:

The Grand Total, Help Text, and buttons have positions relative to the size of the grid. They all appear under the grid. The more rows that go into the grid, the lower each of the other controls moves down. The column headings are the only controls you will be adding with a fixed position.

Finding the Best Way to Relate Tables

The IntraBuilder database classes provide four ways to relate tables. Each relation technique relies on different properties. You can mix and match each option as required. The four relation-forming properties are sql, masterRowset, masterSource, and beforeGetValue.

Let's take a closer look at each relation technique so that we can find the approach that works best for the Cart form. To create a meaningful relation, the Cart table needs to have at least two rows. You can use the Add to Cart button on the Detail form to add rows, or you can add them manually. If you do add them manually, make sure the ISBN numbers are valid. For this exercise, the user, data, and quantity field values are irrelevant. Figure 12.1 shows the five Cart rows used for this text.

Figure 12.1 : Viewing the Cart table in Delphi's Database Desktop.

The sql property lets you join more than one table in a single query. You can create the query, assign the sql property, and activate the query in a single statement. Listing 12.1 shows an example of using the sql property to relate tables. The script is included on the CD-ROM as relate1.js.


Listing 12.1. Relate1.js-Using the sql property to relate tables.

 1: q = new Query('SELECT cart.isbn, title.title ' +

 2:               'FROM "cart.dbf" cart, "title.dbf" title ' +

 3:               'WHERE cart.isbn = title.isbn') ;

 4: _sys.scriptOut.clear() ;

 5: while (! q.rowset.endOfSet)

 6: {

 7:   _sys.scriptOut.writeln("ISBN: " + q.rowset.fields[0].value +

 8:                        "  TITLE: "+ q.rowset.fields[1].value) ;

 9:   q.rowset.next() ;

10: }


Lines 1 through 3 create an instance of the Query class. The SQL command is passed as a string to the Query constructor. This automatically sets the sql property and activates the query. The SQL command creates alias names for the dBASE tables. In this case, the alias names match the root of the filename. However, you can also use an alias to shorten the SQL command. For example, the following SQL command does the same thing using t and c in place of title and cart.


SELECT c.isbn, t.title FROM "cart.dbf" c, "title.dbf" t WHERE c.isbn = t.isbn

The remainder of the script prints out values to the Script Pad. Line 4 clears the contents of the results pane.


_sys.scriptOut.clear() ;

The while loop goes through each row of the view and prints the values of the ISBN and TITLE fields. You can check the Script Pad to see whether the relation is working properly.


while (! q.rowset.endOfSet)

{

  _sys.scriptOut.writeln("ISBN: " + q.rowset.fields[0].value +

                       "  TITLE: "+ q.rowset.fields[1].value) ;

  q.rowset.next() ;

}

Before running the Relate1 JavaScript, open the Script Pad. Figure 12.2 shows the results for a Cart table containing five rows. Listings 12.2, 12.3, and 12.4 contain slightly modified versions of the output routine.

Figure 12.2 : Verifying results with the Script Pad.

The next script, shown in Listing 12.2, increases in JavaScript complexity while using much simpler SQL commands. The script is more representative of dropping two tables onto a form and using the Inspector to relate them. Relating queries involves two properties: masterRowset and masterFields. The masterRowset property points from the child query to the parent query. The parent is the controlling query. When the row pointer moves in the parent, the pointer also moves in the child. However, the child row pointer can move without affecting the parent row pointer. This enables you to move among multiple child rows for a single parent.

A parent and child table relation exists when a row in one table corresponds to no row, one row, or more rows in another table. If the child table can contain more than one related row, the relation is a one-to-many relation. A common example is between a customer table and an invoice table. If you navigate to a different customer, the invoice number must also be different. A customer can have multiple invoices, so that you can navigate through different invoices without changing customers.


Listing 12.2. Relate2.js-Using masterRowset to relate queries.

 1: q1 = new Query('SELECT * FROM "cart.dbf"') ;

 2: with ( q2 = new Query('SELECT * FROM "title.dbf"') )

 3: {

 4:   rowset.masterRowset = q1.rowset ;

 5:   rowset.masterFields = "ISBN" ;

 6: }

 7: _sys.scriptOut.clear() ;

 8: while (! q1.rowset.endOfSet)

 9: {

10:   _sys.scriptOut.writeln("ISBN: " + q1.rowset.fields["ISBN"].value +

11:                        "  TITLE: "+ q2.rowset.fields["TITLE"].value) ;

12:   q1.rowset.next() ;

13: }


The relate2.js script starts by creating two simple queries and relating them. Line 1 creates the q1 query that points to the Cart table. The child query is defined as q2 on lines 2 through 6. The with block assigns two properties of the child query to form the relation. In this case, the with block might actually complicate the script. You could substitute the following three statements for lines 2 through 6:

q2 = new Query('SELECT * FROM "title.dbf"')
q2.rowset.masterRowset = q1.rowset ;
q2.rowset.masterFields = "ISBN" ;

The masterFields property must match an existing index in the Title table. When you set masterFields, IntraBuilder looks for a matching field in the parent table and a matching index in the child table. If an index is not found for an identically named field in the child table, a runtime error occurs, as shown in Figure 12.3.

Figure 12.3 : An error when relating tables without an index.

If the field names of the parent and child tables do not match, you can force the index of the child. For example, if the Title table used ISBNUMBER as the ISBN field name and index name, it could still relate to the Cart table. The trick is to explicitly set the indexName before setting the masterFields property, like this:


q2 = new Query('SELECT * FROM "title.dbf"')

q2.rowset.indexName = "ISBNUMBER"

q2.rowset.masterRowset = q1.rowset ;

q2.rowset.masterFields = "ISBN" ;

The relation between the Cart table and Title table is a one-to-one relation and not the one-to-many that typically occurs between parent and child. So does it matter which table is the master? Absolutely. If you were to reverse the relation as shown in the next code segment, the first part of the script would have no complaints.


q1 = new Query('SELECT * FROM "title.dbf"') ;

with ( q2 = new Query('SELECT * FROM "cart.dbf"') )

{

 rowset.masterRowset = q1.rowset ;

 rowset.masterFields = "ISBN" ;

}

However, when the script reached the while loop, it would go through every row in the Title table. As soon as it found a book that no one had ordered, you would get an endOfRowset error.

If you modify the loop to travel through the child query, you would find it to be a short trip. The following code never goes beyond a single title. Although you can navigate around in the child rows, you can navigate only to other rows with the same parent. This would list a single title as many times as it has been ordered by different shoppers.


while (! q2.rowset.endOfSet)

{

 _sys.scriptOut.writeln("ISBN: " + q2.rowset.fields["ISBN"].value +

            " TITLE: "+ q1.rowset.fields["TITLE"].value) ;

 q2.rowset.next() ;

}

Then again, if you don't modify anything, there still would be something interesting to note. The resulting order of the SQL join differs from the masterRow join. The masterRow join moves through the Cart table in the physical or natural order of the dBASE table.


ISBN: 0-672-30877-0 TITLE: Visual dBASE 5.5 Unleashed

ISBN: 1-56529-757-1 TITLE: Delphi By Example

ISBN: 0-672-30914-9 TITLE: Delphi 2: Developer's Guide

ISBN: 0-672-30802-9 TITLE: Tom Swan's Mastering Borland C++ 5

ISBN: 1-57521-104-1 TITLE: Teach Yourself JBuilder in 21 Days

The SQL results are based on the physical order of the Title table. In the Title table, the books are physically grouped by product in the following order: Visual dBASE, JBuilder, Delphi, Borland C++. The relate2.js script creates the join with the same groupings.


ISBN: 0-672-30914-9 TITLE: Delphi 2: Developer's Guide

ISBN: 0-672-30877-0 TITLE: Visual dBASE 5.5 Unleashed

ISBN: 1-57521-104-1 TITLE: Teach Yourself JBuilder in 21 Days

ISBN: 1-56529-757-1 TITLE: Delphi By Example

ISBN: 0-672-30802-9 TITLE: Tom Swan's Mastering Borland C++ 5


Listing 12.3. Relate3.js-Using masterSource to create a set-oriented relation.

 1: q1 = new Query('SELECT * FROM "cart.dbf"') ;

 2: with ( q2 = new Query())

 3: {

 4:   masterSource = q1.rowset ;

 5:   sql = 'SELECT * FROM "title.dbf" WHERE ISBN = :ISBN';

 6:   active = true ;

 7: }

 8: 

 9: _sys.scriptOut.clear() ;

10: while (! q1.rowset.endOfSet)

11: {

12:   _sys.scriptOut.writeln("ISBN: " + q1.rowset.fields["ISBN"].value +

13:                        "  TITLE: "+ q2.rowset.fields["TITLE"].value) ;

14:   q1.rowset.next() ;

15: }


The relate3.js script sets up a relation between two queries without specifying any rowset properties on the child query. Line 4 makes the ISBN parameter available to the SQL command on line 5. When using masterSource parameters, you cannot pass the SQL command to the query constructor because the parameters are undefined until masterSource is set. See Day 21 for more information on creating set-oriented relations.

The relation technique in Listing 12.4 is more complex and flexible than the previous three. It uses a beforeGetValue event, two queries, a calculated field, and a function. It does not rely on any predefined indexes or matching field names.


Listing 12.4. Relate4.js-Using beforeGetValue to simulate a relation.

 1: q1 = new Query('SELECT * FROM "cart.dbf"') ;

 2: q2 = new Query('SELECT * FROM "title.dbf"') ;

 3: q1.rowset.fields.add(new CalcField("TITLE")) ;

 4: q1.rowset.fields["TITLE"].beforeGetValue = getTitle ;

 5:

 6: _sys.scriptOut.clear() ;

 7: while (! q1.rowset.endOfSet)

 8: {

 9:  _sys.scriptOut.writeln("ISBN: " + q1.rowset.fields["ISBN"].value +

10:             " TITLE: "+ q1.rowset.fields["TITLE"].value) ;

11:  q1.rowset.next() ;

12: }

13: 

14: function getTitle()

15: {

16:   if (q2.rowset.applyLocate("ISBN = '"+this.parent["ISBN"].value+"'"))

17:   {

18:    return (q2.rowset.fields["TITLE"].value) ;

19:   }

20:   else

21:   {

22:    return ("") ;

23:   }

24: }


The relate4.js script has three main parts. The first part sets up the queries on lines 1 through 4. The second part is output code on lines 6 through 12. The remaining lines define the function that executes during the beforeGetValue event.

After the two query objects are created, line 3 adds a new calculated field to the field list for q1. The fields property of a rowset is a special type of array. The fields property doesn't contain all the methods of a standard array, but it does include methods to add and delete elements. You can use the delete() method on any existing field from the fields array. The add() method allows only the addition of calculated fields.


q1.rowset.fields.add(new CalcField("TITLE")) ;

The fields property has a string index like an associative array and a numeric index like a standard array. The string index is the field name and the numeric index is the field position. Like a standard array, the field position is zero-based.

A zero-based array starts with element 0. If the array contains 10 elements, the valid index values for the array are 0 through 9. JavaScript, Java, and C++ use zero-based arrays. Some other languages, such as dBASE and Basic, work with one-based arrays where the first element has an index of 1.

Calculated fields are fields with values that exist only in memory. They have no connection to a physical table. The value of a physical field is determined by the beforeGetValue event. The name of a calculated field is set by passing a string to its constructor. When you add a calculated field to a fields array, the name of the calculated field becomes a new index to the fields array.

The default value of a calculated field is an empty string. The calculation of new values occurs when the beforeGetValue event calls the getTitle() function. The beforeGetValue event is triggered whenever a JavaScript statement references the field value through a datalink or in an expression. Line 4 assigns the getTitle() function to the beforeGetValue event. The return value of the getTitle() function acts as the field value.


q1.rowset.fields["TITLE"].beforeGetValue = getTitle ;

The getTitle() function performs a lookup in the Title table for the title value. The lookup uses the ISBN value from the Cart table. The value for the ISBN number is referenced through a parent property on line 16. This is possible because the this reference points to the calculated field. The parent of the calculated field is the array containing both it and the ISBN field.

The ISBN value is a character field and must be enclosed in single quotes. When "ISBN = '"+this.parent["ISBN"].value+"'" is passed down to the engine, it becomes ISBN = '0-672-30877-0' for the first book in the Cart table. If the Rowset::applyLocate() is successful, it returns true, and the current row contains the desired title. The title from the q2 is passed back as the value for the title field in q1.


function getTitle()

{

  if (q2.rowset.applyLocate("ISBN = '"+this.parent["ISBN"].value+"'"))

  {

   return (q2.rowset.fields["TITLE"].value) ;

  }

If Rowset::applyLocate() fails, q1 is left without a valid rowset. When the rowset is invalid, it is at endOfSet. A runtime error occurs if you try to reference a field value when a rowset is at endOfSet. To avoid the error, line 22 returns an empty string if the search fails.


  else

  {

   return ("") ;

  }

}

The resulting output from the relate3.js JavaScript is identical to the output using masterFields and masterRowset. So far, one of the first two methods appears to be the best way to go for the Cart form. Because you have existing indexes and matching field names, the sql, masterFields, and masterSource techniques are easier to set up than using beforeGetValue. Let performance be the tie breaker.

Comparing performance of various joins is much simpler than the benchmarks from Day 10. You already have four scripts that get the same results using different techniques. You can run them one at a time to try to determine which is faster. However, with such small data sets, you are not likely to notice any substantial differences. To compensate, you can use bigger data sets or make repetitive runs against the existing data sets. The repetitive run approach is quicker to set up. Listing 12.5 shows the bench2.js script that you can use to compare the join operations.


Listing 12.5. Bench2.js-Timing 100 runs of each table relation script.

 1: _sys.os.delete("bench2.txt") ;

 2: run100("relate1.js") ;

 3: run100("relate2.js") ;

 4: run100("relate3.js") ;

 5: run100("relate4.js") ;

 6:

 7: function run100(cScript)

 8: {

 9:   var start = new Date() ;

10:   for (var i = 0 ; i < 100 ; i++)

11:   {

12:      _sys.scripts.run(cScript) ;

13:   }

14:   writeTime2(cScript, start, new Date())

15: }

16:

17: function writeTime2(cScript, start, end)

18: {

19:   var fOut  = new File(),

20:       elapsed = 0 ;

21:   elapsed = ((end.getHours() * 3600) +

22:        (end.getMinutes()* 60)  +

23:         end.getSeconds() )

24:      -((start.getHours() * 3600) +

25:       (start.getMinutes()* 60)  +

26:        start.getSeconds()) ;

27:   if ( fOut.exists("bench2.txt") )

28:   {

29:      fOut.open("bench2.txt","rw") ;

30:      fOut.seek(0,2) ; // go to end of file.

31:   }

32:   else

33:   {

34:      fOut.create("bench2.txt") ;

35:   }

36:   fOut.puts("***") ;

37:   fOut.puts("Script:    " + cScript) ;

38:   fOut.puts("Seconds:    " + elapsed ) ;

39:   fOut.close() ;

40: }


The bench2.js script simply calls the other four scripts 100 times each and records the results in a text file. Like the bench1.js script from Day 10, this one starts by deleting the last copy of the results file. Lines 2 through 5 call the run100() function with each version of the relation script.


_sys.os.delete("bench2.txt") ;

run100("relate1.js") ;

run100("relate2.js") ;

run100("relate3.js") ;

run100("relate4.js") ;

The run100() function is a simple loop to run a script 100 times. The start and end times are calculated the same way they were for the bench1 script. The writeTime2() function is a simplified version of the writeTime() function in the bench1.js script. For more information on the bench1.js script and writeTime(), refer to Day 10.


function run100(cScript)

{

  var start = new Date() ;

  for (var i = 0 ; i < 100 ; i++)

  {

     _sys.scripts.run(cScript) ;

  }

  writeTime2(cScript, start, new Date())

}

If I had to make a bet before running the test, I definitely would have placed the sql technique as faster than using beforeGetValue. Fortunately, no one took me up on the bet. The results are shown in Table 12.1. Even with all the overhead of the extra function call, using beforeGetValue was much faster than using sql. The fastest technique uses masterFields to create the join.

Table 12.1. The results from bench2.txt.

Script
Technique
Seconds
relate1.js
sql
29
relate2.js
masterFields
18
relate3.js
masterSource
79
relate4.js
beforeGetValue
22

The graph in Figure 12.4 shows a more visual representation of the results. Here again, you can see that masterFields is the best way to join dBASE tables in the Cart form.

Figure 12.4 : Graphing the second benchmark (shorter bars are faster).


NOTE
The results shown in Table 12.1 and Figure 12.4 are not applicable to joins using ODBC or SQL-Link drivers. Although results vary from one database server to another, almost all will join tables fastest using the sql and masterSource properties.

The Visual Layout of the Cart Form

It is time to set up a framework for the Cart form by creating a layout with a few controls and two related queries. Here you will add the labels for the grid heading and a single row of the grid. Follow these steps:

  1. Open the Cart form in the Form Designer. The current version is the stub form from Day 11.
  2. This form needs a good deal of horizontal space. Set the width of the form to 75.
  3. Drop the Title and Cart tables onto the form to create two query objects. Remove the full path from the sql property of both new queries.
  4. From the Inspector drop-down list box, select parent.title1.rowset. You also can click on the rowset tool button when inspecting the title1 query to get to parent.title1.rowset.
  5. Locate the masterRowset property and use the drop-down list box to select the cart1 query. After you set the masterRowset property, a drop-down list box becomes available for the masterFields property.
  6. Use the drop-down list box to set the masterFields property to ISBN. As soon as you set the masterFields property, the indexName property changes to ISBN. (See Figure 12.5.)
    Figure 12.5 : Relating queries with the Inspector.

  7. Add five HTML controls for the grid column headings. Set the properties as listed in Table 12.2.

Table 12.2. The HTML field label controls.

NameText
Height
Left
Top
Width
labelISBN<H4>ISBN</H4>
1
1
3.5
15
labelQty<H4>Qty</H4>
1
17
3.5
5
labelTitle<H4>Title</H4>
1
23
3.5
27
labelPrice<H4>Price</H4>
1
51
3.5
9
labelTotal<H4>Total</H4>
1
61
3.5
9

  1. Add four more HTML controls that you will use to display values from the table. The QTY field is not included in this group. You can leave default values for the text properties. Use the values in Table 12.3 as a guide.

Table 12.3. The HTML field values controls.

Name
Height
Left
Top
Width
valueISBN
2
1
4.5
15
valueTitle
2
23
4.5
27
valuePrice
2
51
4.5
9
valueTotal
2
61
4.5
9

  1. Add a Text control for the Qty field. Name the field textQty and position it at left 17, top 4.5, and width 4.
  2. Set the template property to 999. You can enter a template directly or use the Template Property Builder as shown in Figure 12.6.
    Figure 12.6 : Using a template for the textQty control.

This is a good time to save the form and see how things are lining up for the browsers. The next group of controls have positions relative to the grid. If the grid does not look appropriate when viewed through a browser, the rest of the controls will also need adjustment. Figure 12.7 shows the Cart form in Netscape Navigator.

Figure 12.7 : Viewing an empty Cart form grid with Navigator.

The values that appear in the first row are default control names such as HTML7 and Text1. These values vary depending on the order in which you placed and renamed each control. You will soon add code to overwrite each default value with a field value. At this point, there is no need to make any modifications to the value or text properties directly through the Form Designer.

The next step in the form's evolution is to add the methods required to show the row data. There are also a few more controls to drop on the form. Follow these steps to add some help text, an update button, a reset button, and some JavaScript:

  1. Reopen the Cart form in the Form Designer. Remember to shut down all IntraBuilder Agents before returning to the design session.
  2. Drop on another HTML control with the following properties: height 2, left 1, top 11, width 70.
  3. Change the name to htmlHelp.
  4. Open the Text Property Builder for htmlHelp and enter the following:
    To change the quantity, change the number in the Qty text box next for
    the book. To cancel a particular book, type a "0" in the text box.
  5. Add a new button with the following position properties: left 1, top 13, width 12.
  6. Change the button name to buttonUpdate and change the text to Update.
  7. Add a Reset button next to the Update button. The Reset button has a switch icon on the Component Palette, but it looks like a regular button when dropped on the form. Use the following position properties: left 15, top 13, width 12. You can leave the Reset name and text properties as they are.
  8. Add two more HTML controls for the Grand Total value. Use the property values shown in Table 12.4.

Table 12.4. The HTML Grand Total controls.

NameText
Height
Left
Top
Width
labelGrandGrand Total
1
40
13
20
valueGrand(default)
1
61
13
9

  1. Right align the values within the following controls: labelPrice, labelTotal, valuePrice, valueTotal, and valueGrand. Aligning values within a control is not the same as aligning the control itself. To align values, set the alignment property. Use the value 2 to right-align.
    At this point, the Cart form should resemble the one in Figure 12.8. That about does it for any visual design work for this form. The remainder of the Cart form development is centered on JavaScript methods.
    Figure 12.8 : Using the Form Designer to lay out the Cart form.

  2. Create a code block for the form's onServerLoad event. Enter the following into the Inspector without creating a linked method:
    {; class::ShowBooks(this)}
  3. Before saving the form, add the following unlinked method. From the menu, select Methods|New Method. Remove the {Export} comment, rename the method to ShowBooks, and enter the JavaScript function shown in Listing 12.6.


Listing 12.6. The single row version of the ShowBooks() method.

 1: function ShowBooks(thisForm)

 2: {

 3:   var titleRow = thisForm.title1.rowset,

 4:       cartRow = thisForm.cart1.rowset ;

 5:   with (thisForm)

 6:   {

 7:      valueISBN.text = titleRow.fields["ISBN"].value ;

 8:      textQty.value  = cartRow.fields["QTY"].value ;

 9:      valueTitle.text = titleRow.fields["TITLE"].value ;

10:      valuePrice.text = titleRow.fields["PRICE"].value ;

11:   }

12: }


The ShowBooks() method contains three shorthand property references. Lines 3 and 4 create two temporary references for the query rowsets. Whenever a method references the same rowset more than three times, it is a good idea to make a shorthand reference. This can substantially reduce the length of assignment statements, making the script easier to read. If you do create shorthand references, be sure to include some indication of what type of class you are pointing to in the shorthand name. In this function, each shorthand reference ends with Row to indicate rowset. You could also begin each rowset reference with the letter r.


var titleRow = thisForm.title1.rowset,

     cartRow = thisForm.cart1.rowset ;

The third shortcut is through the with block on lines 5 through 11. The block assigns values to each grid control that relates directly to a field value. The three HTML controls derive values from the Title table. The Text control works with the QTY field in the Cart table.

Notice that the HTML and Text controls use different properties to determine what characters appear in the browser. With an HTML control, the critical property is text. HTML controls do not have a dataLink property. Controls that have a dataLink property also have a value property that you can use to set the display value. Text controls have the dataLink and value properties.


with (thisForm)

{

   valueISBN.text = titleRow.fields["ISBN"].value ;

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

   valueTitle.text = titleRow.fields["TITLE"].value ;

   valuePrice.text = titleRow.fields["PRICE"].value ;

}

In this application, there always is a matching ISBN in both tables. The masterRowset relation forces the ISBN to be the same for both queries. Line 7 could also be written like this:


valueISBN.text = cartRow.fields["ISBN"].value ;

If you now run the application, the Cart form always displays the book contained in the first row of the Cart table. The Reset button already works. If you change the QTY field and click the Reset button, the value reverts back to the original QTY value. Figure 12.9 shows the first row of values through Internet Explorer.

Figure 12.9 : The Cart form showing values for the first row of the Cart table.

Calculating Totals

The Cart form currently displays default text values for the total and grand total values. The formula for the total is price multiplied by quantity. The grand total is the sum of all grid totals. With a single row grid the total is always equal to the grand total.

Two events require a calculation of both totals. The first calculation event is the onServerLoad. Currently, onServerLoad calls ShowBooks(). All calculation code belongs in the ShowBooks() method. The second calculation event is the Update button's onServerClick. If a shopper modifies a quantity, the totals need to reflect the change. This event can also call ShowBooks(), but first it must update the Cart table with the new QTY value.

The current version of the form has no direct links between methods and events. The only non-null event is for the form's onServerLoad. The completed form has two linked methods: Form_onServerLoad and buttonUpdate_onServerClick. You need to create both to create working totals. Although it is not really a visual task, creating linked events is easier in the Form Designer than in the Script Editor. Use the following steps to set a title and add three methods to the Cart form:

  1. Open the Cart form in the Form Designer.
  2. Change the form title to Boingo's Books - Shopping Cart.
  3. Use the tool button to overwrite the form's onServerLoad code block with a new method. Enter the method as shown:
    function Form_onServerLoad()
    {
    this.buttonCart.visible = false ;
    class::ShowBooks(this) ;
    }
  4. Select the Update button and create the following new method for the onServerClick event:
    function buttonUpdate_onServerClick()
    {
    var cartRow = this.form.cart1.rowset ;
    cartRow.fields["QTY"].value = form.textQty.value ;
    cartRow.save() ;
    class::ShowBooks(this.form) ;
    }
    This method provides the write side of the datalink simulation. Notice that it is not enough to update the value. The rowset must be committed with Rowset::save(). After saving the new value, new totals can be calculated in the ShowBooks() method.
  5. Open ShowBooks() from the Method Editor. Modify the method as shown in Listing 12.7. Save and run the form when you're done.


Listing 12.7. ShowBooks() with totaling statements shown in bold.

 1: function ShowBooks(thisForm)

 2:   {

 3:   var titleRow = thisForm.title1.rowset,

 4:       cartRow = thisForm.cart1.rowset,

 5:       nTotal  = 0,

 6:       nGrand  = 0 ;

 7:   with (thisForm)

 8:   {

 9:      nTotal = cartRow.fields["QTY"].value *

10:          titleRow.fields["PRICE"].value ;

11:      nGrand += nTotal ;

12:      valueISBN.text = titleRow.fields["ISBN"].value ;

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

14:      valueTitle.text = titleRow.fields["TITLE"].value ;

15:      valuePrice.text = titleRow.fields["PRICE"].value ;

16:      valueTotal.text = nTotal ;

17:   }

18:   thisForm.valueGrand.text = nGrand ;

19: }


The ShowBooks() method now updates the total and grand total HTML controls. Line 11 sums the grand total using the += operator. In the method's current state, the = operator would work just as well. The += is typically used to sum values within a loop.

In the next incarnation of the Cart form, the entire with block will be in a loop. Line 18 could also be in the current with block. However, the grand total value is not part of the grid and must be summed outside the loop. To make it easier to add the grid loop, the grand total value is already outside the with block.

If you run the form now, you can modify the quantity and watch the totals update. Figure 12.10 shows the Cart form after updating the quantity to five. Your form should now match the Day 12 cart1.jfm file on the CD-ROM.

Figure 12.10 : The Cart form with working totals.

Making a Data-Driven Grid

The key to creating a data-driven grid is to use array elements as the control references. Arrays make it easy to modify sets of controls based on ever-changing data. Unfortunately, there is no visual way to do this through the Form Designer. The first row of grid controls, which you created this morning, must now be removed from the constructor. But don't open the Form Designer and start hitting the Delete key just yet. You can reuse the generated code inside your own method. With a few tweaks here and there, you can turn each control into an array of controls.

Data-driven describes highly flexible components and forms that adjust to best fit table values at runtime. A simple component such as a Text control with a dataLink property set to a field has minimal data-driven functionality. A grid that expands or contracts to match the current number of rows in a predefined table is moderately data-driven. A highly data-driven grid might generate columns to match any given table.

Before you start on the coding, reduce the number of rows in the Cart table to three. If you have less, it will be hard to tell if the grid is working. Any more than three, and you might need to scroll down to use the buttons.

Constructing the Grid

To make the modifications in this section, open the Cart form in the Script Editor. The File Open dialog that opens from the menu bar does not have an option to open a form in the Script Editor. However, such an option is available from a shortcut menu in the IntraBuilder Explorer. Another way to open the Cart form in the Script Editor is by entering the following in the Script Pad:


_sys.scripts.design("cart.jfm") ;

Create a new MakeGrid() method. To create a method in the Script Editor, press Ctrl+PgDn to move to the bottom of the script. Look for the closing bracket of the class definition. If you add function definitions below the bracket, they are not methods. Typically, methods also are indented three spaces; however, functions are not. Add the MakeGrid() method right above the closing bracket, as shown in bold in the following code. The notAMethod() function is shown for illustrative purposes only and is not required by the form.


  function buttonUpdate_onServerClick()

  {

   var cartRow = this.form.cart1.rowset ;

   cartRow.fields["QTY"].value = form.textQty.value ;

   cartRow.save() ;

   class::ShowBooks(this.form) ;

  }

  function MakeGrid()

  {

  }

}

function notAMethod()

{

  alert("Hi, I'm just a regular function") ;

}

The next step is to cut the grid control definitions from the constructor and paste them into the MakeGrid() method. If you scroll back toward the top of the script, you will find the definitions for each of the grid row controls. The following segment is the definition for the valueISBN control:


with (this.valueISBN = new HTML(this)){

   height = 2;

   left = 1;

   top = 4.5;

   width = 15;

   color = "black";

   text = "HTML7";

}

Move the definition code for valueISBN, textQty, valueTitle, valuePrice, and valueTotal into the MakeGrid() method. If you are lucky, all the definitions will be consecutive, and you can use a single cut and paste.

After they are all moved, you can tidy up the MakeGrid() method by indenting each control definition three spaces and removing unnecessary property assignments. Because black is the default HTML color, you can remove it from each HTML control with block. The text property of each HTML control is redundant, as is the value property for the Text control. The following rendition of the MakeGrid() method contains only the required property settings.


function MakeGrid()

{

   with (this.valueISBN = new HTML(this)){

      height = 2;

      left = 1;

      top = 4.5;

      width = 15;

   }



   with (this.textQty = new Text(this)){

      left = 17;

      top = 4.5;

      width = 4;

      template = "999";

   }



   with (this.valueTitle = new HTML(this)){

      height = 2;

      left = 23;

      top = 4.5;

      width = 27;

   }





   with (this.valuePrice = new HTML(this)){

      height = 2;

      left = 51;

      top = 4.5;

      width = 9;

      alignHorizontal = 2;

   }



   with (this.valueTotal = new HTML(this)){

      height = 2;

      left = 61;

      top = 4.5;

      width = 9;

      alignHorizontal = 2;

   }

}

If you run the form at this point, you get an error. The runtime error occurs when ShowBooks() tries to assign a value to one of the controls you just ripped out of the constructor. To remedy the situation, the new MakeGrid() method needs to run before ShowBooks(). From within the Script Editor, locate the Form_onServerLoad() method and add the class::MakeGrid() call, as shown in this segment:


function Form_onServerLoad()

{

  this.buttonCart.visible = false ;

  class::MakeGrid() ;

  class::ShowBooks(this) ;

}

Now the form will run as it did before. Try running it through the Designer to shake out any bugs that might have crept in so far. One common mistake is to accidentally use Copy instead of Cut when moving the controls. If you copy the lines, no runtime error occurs. The form simply creates an extra control that is released as soon as you make another control with the same name. It is not a critical error, but it does eat some extra resources and processing time.

After you have the form running as before, it is time to transform the value control references into arrays. From the Script Editor, you can complete the MakeGrid() method as shown in Listing 12.8.


Listing 12.8. The complete MakeGrid() method.

 1: function MakeGrid()

 2: {

 3:    var nCount = this.cart1.rowset.count() ;

 4:    this.valueISBN  = new Array(nCount) ;

 5:    this.textQty    = new Array(nCount) ;

 6:    this.valueTitle = new Array(nCount) ;

 7:    this.valuePrice = new Array(nCount) ;

 8:    this.valueTotal = new Array(nCount) ;

 9:

10:    for ( var i = 0 ; i < nCount ; i++ ) {

11:       with (this.valueISBN[i] = new HTML(this)){

12:          height = 2;

13            left = 1;

14:          top = 4.5 + ( i * 2 );

15:          width = 15;

16:       }

17:

18:       with (this.textQty[i] = new Text(this)){

19:          left = 17;

20:          top = 4.5 + ( i * 2 );

21:          width = 4;

22:          template = "999";

23:       }

24:

25:       with (this.valueTitle[i] = new HTML(this)){

26:          height = 2;

27:          left = 23;

28:          top = 4.5 + ( i * 2 );

29:          width = 27;

30:       }

31:

32:       with (this.valuePrice[i] = new HTML(this)){

33:          height = 2;

34:          left = 51;

35:          top = 4.5 + ( i * 2 );

36:          width = 9;

37:          alignHorizontal = 2;

38:       }

39:

40:       with (this.valueTotal[i] = new HTML(this)){

41:          height = 2;

42:          left = 61;

43:          top = 4.5 + ( i * 2 );

44:          width = 9;

45:          alignHorizontal = 2;

46:       }

47:    }

48: }


The complete MakeGrid() method creates a new set of controls for each row in the Cart query. The only difference between one set of controls and the next is the value of the top property. No field values are assigned in MakeGrid().

Line 3 gets a count of the rows available to the Cart query. You can use the filter property to reduce the count. Lines 4 through 8 use the count to determine the array size for five new arrays. The array reference names match the names previously used for the grid controls.


var nCount = this.cart1.rowset.count() ;

this.valueISBN  = new Array(nCount) ;

this.textQty    = new Array(nCount) ;

this.valueTitle = new Array(nCount) ;

this.valuePrice = new Array(nCount) ;

this.valueTotal = new Array(nCount) ;

A for loop encloses the control definitions. Adding the array index after the control name allows the same reference name to work with a set of controls. Previously, this.valueISBN was the one and only ISBN value control. Now this.valueISBN is an array of controls, where this.valueISBN[1] is one of several ISBN value controls.

The top property also is relative to the value of i. Each set of controls appears slightly below the previous row. The first set is at 4.5, the second is at 6.5, and so on.


for ( var i = 0 ; i < nCount ; i++ ) {

   with (this.valueISBN[i] = new HTML(this)){

      height = 2;

      left = 1;

      top = 4.5 + ( i * 2 );

      width = 15;

 }

You can run the form to see how the grid displays. Because the ShowBooks() method is not in synch with the control arrays, the rows display default values in place of values from the queries. Figure 12.11 shows how the form looks when run in the Designer with three rows in the Cart table.

Figure 12.11 : The first visible signs of the grid.

Check your running form against Figure 12.11 to verify that the MakeGrid() method is working correctly. If you have more than three rows in the table, the grid might overlap the help text, the update button, and the reset button.

Filling the Grid

The process of filling the grid is similar to creating it. In the MakeGrid() method, a for loop cycles through the control set once per row in the Cart table. ShowBooks() needs a similar while loop to assign field values. Reopen the Cart form in the Script Editor and locate the ShowBooks() method. Rewrite the method as shown in Listing 12.9.


Listing 12.9. The complete ShowBooks() method.

 1: function ShowBooks(thisForm)

 2: {

 3:    var titleRow = thisForm.title1.rowset,

 4:        cartRow = thisForm.cart1.rowset,

 5:        nTotal  = 0,

 6:        nGrand  = 0,

 7:        i    = 0 ;

 8:

 9:    cartRow.first() ;

10:    while ( ! cartRow.endOfSet ) {

11:       with (thisForm) {

12:          nTotal = cartRow.fields["QTY"].value *

13:             titleRow.fields["PRICE"].value ;

14:          nGrand += nTotal ;

15:          valueISBN[i].text = titleRow.fields["ISBN"].value ;

16:          textQty[i].value  = cartRow.fields["QTY"].value ;

17:          valueTitle[i].text = titleRow.fields["TITLE"].value ;

18:          valuePrice[i].text = titleRow.fields["PRICE"].value ;

19:          valueTotal[i].text = nTotal ;

20:       }

21:       i++ 

22:       cartRow.next() ;

23:    }

24:

25:    thisForm.valueGrand.text = nGrand ;

26:    thisForm.htmlHelp.top     = 4.5 + ( i * 2 ) ;

27:    thisForm.buttonUpdate.top = 7   + ( i * 2 ) ;

28:    thisForm.reset1.top       = 7   + ( i * 2 ) ;

29:    thisForm.labelGrand.top   = 7   + ( i * 2 ) ;

30:    thisForm.valueGrand.top   = 7   + ( i * 2 ) ;

31: }


The revised ShowBooks() method is in synch with the control arrays. The major changes include the while loop on line 10 and control repositioning code on lines 26 through 30. Another change is on line 7, which creates a variable for the array index. MakeGrid() created and incremented the array index as part of the for loop. Because a while loop does not include incrementing as part of its structure, ShowBooks() must create and increment the index array apart from the loop.


var titleRow = thisForm.title1.rowset,

    cartRow = thisForm.cart1.rowset,

    nTotal  = 0,

    nGrand  = 0,

    i    = 0 ;

Line 9 calls Rowset::first() to reset the row pointer at the first row in the current view. By moving to the first row and then looping until endOfSet, the while loop is guaranteed to hit every available row. When ShowBooks() is called from the onServerLoad event, the row pointer is already at the first row. In that case, the call is unnecessary. It becomes important when the method is called from the Update button. After ShowBooks() runs once, the pointer is left at endOfSet. If you leave out the call to Rowset::first(), calling ShowBooks() a second time causes the while loop never to execute.


cartRow.first() ;

while ( ! cartRow.endOfSet ) {

The only change inside the with block is the addition of the array index after the control name. The logic to calculate totals is the same as before.


valueTotal[i].text = nTotal ;

Lines 26 through 30 reposition the help text, Update button, Reset button, grand total label, and grand total value. Each of these controls has a position under the grid. As the grid expands, it pushes these controls down. In the following segment, i is equal to the number of rows in the grid.


thisForm.htmlHelp.top     = 4.5 + ( i * 2 ) ;

thisForm.buttonUpdate.top = 7   + ( i * 2 ) ;

thisForm.reset1.top       = 7   + ( i * 2 ) ;

thisForm.labelGrand.top   = 7   + ( i * 2 ) ;

thisForm.valueGrand.top   = 7   + ( i * 2 ) ;

With the completed ShowBooks() method, you can view data from more than one row at a time. Figure 12.12 shows the Cart form running in the Designer. The form would be complete if it were not for that Update button. The update stopped working when the grid went from simple controls to arrays of controls. You will correct that in the third and final part of the Cart form trilogy. The form should now match the Day 12 cart2.jfm file on CD-ROM.

Figure 12.12 : Viewing data for three books at once.

Adding Dynamic Updates

To handle dynamic updates, the Cart form needs to get a lot smarter about how it displays the grid. Right now it works great to display a fixed set of rows. However, dynamic updates require the grid to handle changes to the number of rows. The Cart form does not allow for new rows, but it does let the shopper remove an item by entering zero in the quantity field. When the shopper hits the Update button, any rows with a zero quantity should disappear from the grid.

If shoppers can delete a single row, they can delete all the rows. The Cart form has to handle having the grid go to zero rows. Remember that with the Quick form, this situation is handled with a Message form. If a query comes up empty, the Results form calls the Message form instead of the Results form. This is pretty easy, but it only works when the shopper is already moving to another form. The Update button on the Cart form is a self-contained process. Shoppers do not expect to move to a new form when they click the Update button. After the problem of an empty cart is solved, the update operation becomes a snap.

Dealing with an Empty Cart

Currently, the Update button and the Form_onServerLoad() method call on ShowBooks() to display the grid values. This works great when you know there are books to show. It does nothing if there are no rows left in the query. Because ShowBooks() is so good at what it does, you can keep it just the way it is. Instead of changing ShowBooks(), create three new methods that isolate ShowBooks() from the empty cart scenario. In this section, you will create the following three new methods:

It is time to open up the Cart form in the Script Editor and start coding away. Move to the bottom of the class definition and add the BlankGrid(), HideBooks(), and PaintForm() methods. Place them all before the closing bracket for the form class. Use Listings 12.10, 12.11, and 12.12 as your guides.

TIP
After adding each function, select Script|Compile to check for syntax errors. If an error is found, note the line number from the Compile dialog and cancel the compile. Do not select Fix if you already have the Script Editor open. Selecting Fix opens another window with the same file. Things can get confusing when you have multiple copies of the script open.


Listing 12.10. The complete BlankGrid() method.

 1: function BlankGrid(thisForm)

 2: {

 3:    for ( var i = thisForm.cart1.rowset.count() ;

 4:       i < thisForm.valueISBN.length ;

 5:       i++ ) {

 6:       thisForm.valueISBN[i].visible  = false ;

 7:       thisForm.textQty[i].visible    = false ;

 8:       thisForm.valueTitle[i].visible = false ;

 9:       thisForm.valuePrice[i].visible = false ;

10:       thisForm.valueTotal[i].visible = false ;

11:    }

12: }


This method determines what rows to blank out in the for statement. All the arrays are the same length. The length of any array equals the number of rows existing in the view at the time the form opens. The for loop executes only if the current row count is less than the original count. Line 3 sets the initial array index to the current row count. Because arrays are zero-based and rows are not, the row count is one beyond the highest array index needed to contain all rows.


for ( var i = thisForm.cart1.rowset.count() ;

   i < thisForm.valueISBN.length ;

   i++ ) {

If the form opens with five rows, the length of the array is 5 and the valid array index values are from 0 to 4. If the user deletes two rows, the row count becomes 3 and the number of elements in the array stays at 5. With only three rows, the controls referenced by the array indexes of 3 and 4 need to be blanked. With literal values, the for statement becomes the following:


for ( var i = 3 ; i < 5 ; i++ ) {

After a control is invisible, it never returns to the visible grid. Because this routine is repeatedly called, the same controls will have their respective visible properties set to false. There is no harm done in hiding a control that is already hidden.


Listing 12.11. The complete HideBooks() method.

 1: function HideBooks(thisForm)

 2: {

 3:    with (thisForm) {

 4:       labelISBN.width = 50 ;

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

 6:       labelQty.visible     = false ;

 7:       labelTitle.visible   = false ;

 8:       labelPrice.visible   = false ;

 9:       labelTotal.visible   = false ;

10:       htmlHelp.visible     = false ;

11:       buttonUpdate.visible = false ;

12:       reset1.visible       = false ;

13:       labelGrand.visible   = false ;

14:       valueGrand.visible   = false ;

15:    }

16: }


The HideBooks() method sets several visible properties to false and uses a with block to cut down on the required typing. The only interesting thing occurring here is that the ISBN column heading is transformed into a message line. Line 5 enhances the text with an HTML <H3> tag. This tag usually increases the font size by several point sizes.

After HideBooks() runs, the shopper can use only the Quick, Keyword, Checkout, and Help toolbar buttons to continue. This is one less choice than you have with the Message form.

Be very careful when spelling the visible property. More than one JavaScript programmer has wasted hours tracking down what turned out to be a misspelling. If you accidentally type the property as visable, you create a new property that does nothing.


Listing 12.12. The complete PaintForm() method.

 1: function PaintForm(thisForm)

 2: {

 3:    class::BlankGrid(thisForm) ;

 4:    thisForm.cart1.rowset.first() ;

 5:    if ( ! thisForm.cart1.rowset.endOfSet ) {

 6:       class::ShowBooks(thisForm) ;

 7:    }

 8:    else {

 9:       class::HideBooks(thisForm) ;

10:    }

11: }


The PaintForm() method is called from both the Form_onServerLoad (where the form reference is this) and the Update button's onServerClick, which has a this.form pointer to the form. The PaintForm() method uses a user-defined parameter, called thisForm, to reconcile the difference. The thisForm parameter is passed down to other methods.

Line 3 calls BlankGrid() to clear out any extra rows. It then checks to see whether the Cart query is empty. If so, line 9 calls HideBooks(), and the Cart form turns into a message form. If there are rows to show, line 6 calls ShowBooks() and the shopper is free to modify quantities and remove rows.

To try the three new functions, you will need to modify the Form_onServerLoad() method. Replace the call to ShowBooks() with a call to PaintForm(). If you run the Cart form with this change, the form appears exactly as it did before you did all this work.


function Form_onServerLoad()

{

  this.buttonCart.visible = false ;

  class::MakeGrid() ;

  class::PaintForm(this) ;

}

If you want to test the empty cart condition, add a filter assignment to Form_onServerLoad() or delete all the rows in the Cart table. To simulate an empty Cart table, add the filter before the call to MakeGrid(). Use a filter condition that does not match any rows in the current table. Figure 12.13 shows the Cart form with an empty rowset.

Figure 12.13 : The Cart form after HideBooks() has executed.


function Form_onServerLoad()

{

   this.buttonCart.visible = false ;

   this.cart1.rowset.filter = "CARTUSER = 'not likely'" ;

   class::MakeGrid() ;

   class::PaintForm(this) ;

}

Synching Up Grid Values with Table Values

It is finally time to get back to the long-neglected Update button. In the first incarnation of the Cart form, the Update button could modify the quantity value for the first row in the Cart table. The method had a simple one-to-one relation between the controls and the first row of the table.

To complete the buttonUpdate_onServerClick() method, you need to make it work with any number of rows. Instead of updating the current row, the method must locate the appropriate rows while traversing an array of ISBN values.

The previous version also assumed that the quantity entered was valid. This time around, you will not give the benefit of the doubt to the shopper. Here are the four categories of values that a shopper can enter into the quantity field:

That's all there is for the design specifications of the Update button. To proceed, open the Cart form in the Script Editor and modify buttonUpdate_onServerClick(), as shown in Listing 12.13.


Listing 12.13. The complete buttonUpdate_onServerClick() method.

 1: function buttonUpdate_onServerClick()

 2: {

 3:    var cartRow  = this.form.cart1.rowset ;

 4:    var nCount   = cartRow.count(),

 5:        nTrueQty = 0 ;

 6:

 7:    for ( var i = 0 ; i < nCount ; i++ ) {

 8:       this.form.textQty[i].template = null ;

 9:       nTrueQty = this.form.textQty[i].value ;

10:       this.form.textQty[i].template = "999" ;

11:       cartRow.applyLocate("ISBN='" + this.form.valueISBN[i].text + "'") ;

12:

13:       if ( ! cartRow.endOfSet ) {

14:          if ( nTrueQty == 0 ) {

15:             cartRow.delete() ;

16:          }

17:          else if ( ( nTrueQty > 999 ) ||

18:                    ( nTrueQty < 0  ) ) {

19:            // do nothing & the value will be restored in ShowBooks()

20:          }

21:          else {

22:             if ( ! ( cartRow.fields["QTY"].value == nTrueQty ) ) {

23:                cartRow.fields["QTY"].value = nTrueQty ;

24:                cartRow.save() ;

25:             }

26:          }

27:       }

28:

29:    }

30:

31:    class::PaintForm(this.form) ;

32: }


Lines 3 and 4 start the method by making a shortcut reference to the Cart query rowset and by figuring out how many rows are visible in the grid. It does this by using two var statements. The statements are separated so that the second var statement can make use of the shortcut reference created in the first.


var cartRow = this.form.cart1.rowset ;

var nCount  = cartRow.count(),

   nTrueQty = 0 ;

The for loop creates an index into all visible grid controls. Line 7 uses the row count to determine when to exit the loop. If you replace nCount with an array length such as this.form.valueISBN.length, the loop processes invisible controls and updates the Cart table with improper values.


for ( var i = 0 ; i < nCount ; i++ ) {

Lines 8 through 10 extract the true value from the current text quantity. When a text control uses a template, only values that conform to the template are accessible through the value property. The textQty controls have a template of 999, making the retrievable range -99 to 999. Any other number evaluates to zero.

The text object internally stores true values that are outside the template range. You can retrieve the true value by temporarily removing the template. Extracting the true value is essential if you want to treat invalid values differently from zero values. In this application, zero values cause a row deletion. It would be unfortunate to delete a row when the quantity field is out of the valid range.


this.form.textQty[i].template = null ;

nTrueQty = this.form.textQty[i].value ;

this.form.textQty[i].template = "999" ;

After establishing the nTrueQty value, the method looks for the row belonging to the current ISBN. Because this is the only routine that can delete a row, the ISBN should always be found. If, through some unforeseen circumstance, the row is not found, no further update processing takes place.


cartRow.applyLocate("ISBN='" + this.form.valueISBN[i].text + "'" ) ;

if ( ! cartRow.endOfSet ) {

Line 14 checks to see whether the shopper entered a zero. If so, the associated row is immediately deleted. The Rowset::save() method is not required to commit a deletion. Rowset::delete() removes the row from the current query rowset and the associated table. You cannot recall the row through IntraBuilder.


if ( nTrueQty == 0 ) {

   cartRow.delete() ;

}

NOTE
Unlike out-of-range numbers, a character string is not stored aside from the template. If the shopper enters FIVE into a quantity field, the value becomes 0. This can be prevented through client-side JavaScript.

When a shopper enters a quantity that is outside the valid range, you don't need to do anything. This is one of those problems that can take care of itself. All the grid values are overwritten by the ShowBooks() method. If you do not store any new values to the table, the previous values reappear. Line 31 calls PaintForm(), which in turn calls ShowBooks() when no updates take place.


else if ( ( nTrueQty > 999 ) ||

          ( nTrueQty < 0  ) ) {

   // do nothing & the value will be restored in ShowBooks()

}

TIP
Always include a comment for empty control blocks. For the most part, JavaScript is easy to read and requires little in the way of internal comments. However, it is a good idea to comment code that appears to be incomplete or in error. If you were reviewing a script with an empty if block, you might think the script was wrong and accidentally remove the empty block. Adding a comment makes the code appear more deliberate.

The update process is optimized by altering only rows where the shopper has modified the quantity. If a shopper clicks the Update button without making any changes to a quantity, the quantity is fine as it is. If the control value is not equal to the field value, the method assigns the new value to the field and commits the change by calling Rowset::save() on line 24.


else {

   if ( ! ( cartRow.fields["QTY"].value == nTrueQty ) ) {

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

      cartRow.save() ;

   }

}

After performing any necessary updates, line 31 calls PaintForm(). Any changes to the row count and values are already committed by the time PaintForm() starts. PaintForm()and its related methods rework the grid to reflect any changes to the table.


class::PaintForm(this.form) ;

With the completed buttonUpdate_onServerClick(), you can test the dynamic updates through the Designer. Try changing some new quantity values, entering some invalid values, and deleting some rows.

If everything is working as expected, you can finally integrate the Cart form into the rest of the application. Up to this point, the Cart form has run from the Help form and ignored the user key value. It also has run directly from the IntraBuilder Explorer. Having the form independent makes development and debugging much easier. You can add one line into Form_onServerLoad() to finish the job. Add the following filter assignment as shown on line 164 of Listing 12.14:


this.cart1.rowset.filter = "CARTUSER = " + "'" + form.user + "'" ;

After you add this line, the cart will be empty each time you start the application from the Help form. As you add books, the cart fills up. Shoppers are in control of their own cart. Figure 12.14 shows the completed form after adding a few books and changing some quantities.

Figure 12.14 : Running the completed Cart form through Navigator.

If things are not going as expected, review Listing 12.14, which shows the complete JavaScript source code for the Cart form. There is no analysis given here because every method has already been analyzed. This listing provides an overview, so that you can see how all the pieces fit together. The exact order of methods and controls is unimportant. The Day 12 cart3.jfm CD-ROM file is a copy of the Cart form in Listing 12.14.


Listing 12.14. Cart.jfm-The final incarnation of the Cart form.

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

  2: // Generated on 01/02/97

  3: //

  4: var f = new cartForm();

  5: f.open();

  6: class cartForm 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 - Shopping Cart";

 14:    }

 15:

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

 17:       left = 70;

 18:       top = 1;

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

 20:       active = true;

 21:    }

 22: 

 23:    with (this.cart1.rowset) {

 24:    }

 25: 

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

 27:       left = 70;

 28:       top = 2;

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

 30:       active = true;

 31:    }

 32: 

 33:    with (this.title1.rowset) {

 34:       indexName = "ISBN";

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

 36:       masterFields = "ISBN";

 37:    }

 38: 

 39:    with (this.labelISBN = new HTML(this)){

 40:       height = 1;

 41:       left = 1;

 42:       top = 3.5;

 43:       width = 15;

 44:       color = "black";

 45:       text = "<H4>ISBN</H4>";

 46:    }

 47: 

 48:    with (this.labelQty = new HTML(this)){

 49:       height = 1;

 50:       left = 17;

 51:       top = 3.5;

 52:       width = 5;

 53:       color = "black";

 54:       text = "<H4>Qty</H4>";

 55:    }

 56: 

 57:    with (this.labelTitle = new HTML(this)){

 58:       height = 1;

 59:       left = 23;

 60:       top = 3.5;

 61:       width = 27;

 62:       color = "black";

 63:       text = "<H4>Title</H4>";

 64:    }

 65: 

 66:    with (this.labelPrice = new HTML(this)){

 67:       height = 1;

 68:       left = 51;

 69:       top = 3.5;

 70:       width = 9;

 71:       color = "black";

 72:       alignHorizontal = 2;

 73:       text = "<H4>Price</H4>";

 74:    }

 75:

 76:    with (this.labelTotal = new HTML(this)){

 77:       height = 1;

 78:       left = 61;

 79:       top = 3.5;

 80:       width = 9;

 81:       color = "black";

 82:       alignHorizontal = 2;

 83:       text = "<H4>Total</H4>";

 84:    }

 85: 

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

 87:       height = 2;

 88:       left = 1;

 89:       top = 11;

 90:       width = 70;

 91:       color = "black";

 92:       text = 'To change the quantity, change the number in the ' + 

 93:              'Qty text box next for the book. To cancel a ' + 

 94:              'particular book, type a "0" in the text box.';

 95:    }

 96: 

 97:    with (this.buttonUpdate = new Button(this)){

 98:       onServerClick = class::buttonUpdate_onServerClick;

 99:       left = 1;

100:       top = 13; 

101:       width = 12;

102:       text = "Update";

103:    }

104: 

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

106:       left = 15;

107:       top = 13;

108:       width = 12;

109:       text = "Reset";

110:    }

111: 

112:    with (this.labelGrand = new HTML(this)){

113:       height = 1;

114:       left = 40;

115:       top = 13;

116:       width = 20;

117:       color = "black";

118:       text = "Grand Total";

119:    }

120: 

121:    with (this.valueGrand = new HTML(this)){

122:       height = 1;

123:       left = 61;

124:       top = 13;

125:       width = 9;

126:       color = "black";

127:       alignHorizontal = 2;

128:       text = "HTML2";

129:    }

130: 

131:    function ShowBooks(thisForm)

132:    {

133:       var titleRow = thisForm.title1.rowset, 

134:           cartRow  = thisForm.cart1.rowset, 

135:           nTotal   = 0, 

136:           nGrand   = 0, 

137:           i        = 0 ;  

138:       cartRow.first() ; 

139:       while ( ! cartRow.endOfSet ) {        

140:          with (thisForm) {

141:             nTotal = cartRow.fields["QTY"].value * 

142:                      titleRow.fields["PRICE"].value ;

143:             nGrand += nTotal ;

144:             valueISBN[i].text  = titleRow.fields["ISBN"].value ;

145:             textQty[i].value   = cartRow.fields["QTY"].value ;

146:             valueTitle[i].text = titleRow.fields["TITLE"].value ;

147:             valuePrice[i].text = titleRow.fields["PRICE"].value ; 

148:             valueTotal[i].text = nTotal ;

149:          }

150:          i++ 

151:          cartRow.next() ; 

152:       }

153:       thisForm.valueGrand.text = nGrand ;

154:       thisForm.htmlHelp.top     = 4.5 + ( i * 2 ) ;

155:       thisForm.buttonUpdate.top = 7   + ( i * 2 ) ;

156:       thisForm.reset1.top       = 7   + ( i * 2 ) ;

157:       thisForm.labelGrand.top   = 7   + ( i * 2 ) ;

158:       thisForm.valueGrand.top   = 7   + ( i * 2 ) ;

159:    }

160: 

161:    function Form_onServerLoad()

162:    {

163:       this.buttonCart.visible = false ;

164:       this.cart1.rowset.filter = "CARTUSER = " + "'" + form.user + "'" ;

165:       class::MakeGrid() ;

166:       class::PaintForm(this) ;

167:    }

168: 

169:    function buttonUpdate_onServerClick()

170:    {

171:       var cartRow  = this.form.cart1.rowset ;

172:       var nCount   = cartRow.count(),

173:           nTrueQty = 0 ;

174:       for ( var i = 0 ; i < nCount ; i++ ) {

175:          this.form.textQty[i].template = null ;

176:          nTrueQty = this.form.textQty[i].value ;

177:          this.form.textQty[i].template = "999" ;

178:          cartRow.applyLocate("ISBN='"+this.form.valueISBN[i].text+"'");

179:          if ( ! cartRow.endOfSet ) {

180:             if ( nTrueQty == 0 ) {

181:                cartRow.delete() ;

182:             }

183:             else if ( ( nTrueQty > 999 ) ||

184:                       ( nTrueQty < 0  ) ) {

185:               // do nothing & the value will be restored in ShowBooks

186:             }

187:             else {

188:               if (! ( cartRow.fields["QTY"].value == nTrueQty )){

189:                   cartRow.fields["QTY"].value = nTrueQty ;

190:                   cartRow.save() ;

191:                }

192:             }

193:          }

194:       }

195:       class::PaintForm(this.form) ;

196:    }

197: 

198:    function MakeGrid()

199:    {

200:       var nCount = this.cart1.rowset.count() ;

201:       this.valueISBN  = new Array(nCount) ;

202:       this.textQty    = new Array(nCount) ;

203:       this.valueTitle = new Array(nCount) ;

204:       this.valuePrice = new Array(nCount) ;

205:       this.valueTotal = new Array(nCount) ;

206:       for ( var i = 0 ; i < nCount ; i++ ) {

207:          with (this.valueISBN[i] = new HTML(this)){

208:             height = 2;

209:             left = 1;

210:             top = 4.5 + ( i * 2 );

211:             width = 15;

212:          }

213:          with (this.textQty[i] = new Text(this)){

214:             left = 17;

215:             top = 4.5 + ( i * 2 );

216:             width = 4;

217:             template = "999";

218:          }

219:          with (this.valueTitle[i] = new HTML(this)){

220:             height = 2;

221:             left = 23;

222:             top = 4.5 + ( i * 2 );

223:             width = 27; 

224:          }

225:          with (this.valuePrice[i] = new HTML(this)){

226:             height = 2;

227:             left = 51;

228:             top = 4.5 + ( i * 2 ) ;

229:             width = 9;

230:             alignHorizontal = 2;

231:          }

232:          with (this.valueTotal[i] = new HTML(this)){

233:             height = 2;

234:             left = 61;

235:             top = 4.5 + ( i * 2 );

236:             width = 9;

237:             alignHorizontal = 2;

238:          }

239:       }

240:    }

241: 

242:    function BlankGrid(thisForm)

243:    {

244:       for ( var i = thisForm.cart1.rowset.count() ;

245:          i < thisForm.valueISBN.length ;

246:          i++ ) {

247:          thisForm.valueISBN[i].visible  = false ;

248:          thisForm.textQty[i].visible    = false ;

249:          thisForm.valueTitle[i].visible = false ;

250:          thisForm.valuePrice[i].visible = false ;

251:          thisForm.valueTotal[i].visible = false ;

252:       }

253:    }

254: 

255:    function HideBooks(thisForm)

256:    {

257:       with (thisForm) {

258:          labelISBN.width = 50 ;

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

260:          labelQty.visible     = false ;

261:          labelTitle.visible   = false ;

262:          labelPrice.visible   = false ;

263:          labelTotal.visible   = false ;

264:          htmlHelp.visible     = false ;

265:          buttonUpdate.visible = false ;

266:          reset1.visible       = false ;

267:          labelGrand.visible   = false ;

268:          valueGrand.visible   = false ;

269:       }

270:    }

271: 

272:    function PaintForm(thisForm)

273:    {

274:       class::BlankGrid(thisForm) ;

275:       thisForm.cart1.rowset.first() ;

276:       if ( ! thisForm.cart1.rowset.endOfSet ) {

277:          class::ShowBooks(thisForm) ;

278:       }

279:       else {

280:          class::HideBooks(thisForm) ;

281:       }

282:    }

283: 

284: }


Summary

Today you learned what can and can't be done in the Form Designer. You also discovered how JavaScript can overcome limitations to the visual design approach. Throughout the day, three distinct versions of the Cart form came into being.

The first was oriented around visual development. You added all the controls through the Form Designer to create a single row of the grid and other supporting controls. The first form provided a simple update capability and a simulated datalink.

The second incarnation of the Cart form moved you from visual-centric development to a JavaScript orientation. You leveraged the day's earlier work and morphed the single row of fixed position controls into dynamically sized arrays of controls. This version showed off the grid but lost the simulated datalink.

By the end of the day, you had added live data updating into the grid. It's interesting to note that the update portion was the simple part, and dealing with the possibility of a shrinking or disappearing rowset was the tougher nut to crack.

Now the shopping cart application has six fully functional forms: Help, Quick, Results, Message, Detail, and Cart. Although this is pretty good progress, two of the five toolbar buttons still lead to errors, and there are only two days left before the week of advanced topics. You should get a good night's rest, so that you are ready for tomorrow's JavaScript adventure: the Keyword form.

Q&A

Q:If I run the final version of the Cart form in the Designer, I can switch from Run mode to Design mode. When that happens, all the grid elements disappear. Why don't the grid controls show up in the Form Designer?
A:Switching to Design mode is really more like closing the current running form and reopening the form in the Designer. This protects the constructor code from any modifications that occur through events. Saving a form forces a regeneration of the constructor code. The Cart form would not run correctly if the grid elements suddenly moved back to the constructor.
Q:After I fixed some errors in the Cart form, IntraBuilder kept running an unfixed version of the code. I had to exit and restart IntraBuilder before it recognized my changes. How can I avoid this situation?
A:If you are fixing errors by selecting the Fix button, IntraBuilder opens a new Script Editor. This can lead to multiple Script Editors with the same script. Closing the current Script Editor does not save any changes if others are still open. Closing IntraBuilder forces all Script Editors to close. Avoid opening a script in more than one window, and you will avoid many instances of this problem.
Q:The QTY field value looks funny because it's over toward the left of the text control. The HTML properties had an alignment property that made the price and total look nicer. Is there any way to right-align the quantity value within the Text control?
A:Although the Text control does not have an alignment property, you can use the template property to provide similar functionality for numbers. Numbers are right-justified within the template. Increasing the width of the template moves the number toward the right of the Text control.
Q:The Cart form had an array of controls working with a single instance of each query. This rendered the dataLink property useless. Could I also create an array of queries so that each row in the grid has a related set of queries with which to datalink?
A:Yes, you can create arrays of queries just as you can create arrays of controls. The advantage of this technique is that all the datalinks are automatic. The drawback is that creating an array of queries is more resource intensive than creating an array of controls. Creating arrays of both could lead to resource strain.

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 properties does the Form Designer always stream out for HTML controls, and which one of them can you safely remove from the constructor?
  2. Why is myArray[myArray.length] likely to cause a runtime error?
  3. In a JavaScript form file, what is the difference between a function definition that occurs before the closing class bracket and one that occurs after the bracket?
  4. What does the value property of a Text control return if the current value does not comply with the template?

Exercises

  1. Currently, shoppers can enter alphabetic values into the quantity fields. JavaScript supports client-side validation through the onChange event of a text control. Modify the Cart form to take advantage of client-side JavaScript and prevent shoppers from entering non-numeric quantity values.
  2. The Text control for the quantity value is wider than it needs to be, but the Title HTML control could use a little more room for long titles. Adjust the grid columns to better accommodate the data. Make sure the controls still align correctly in both Navigator and Internet Explorer.