Passing Variables to JavaScript


ColdFusion developers sometimes get confused about how to make the values of ColdFusion variables available to JavaScript. There are a lot of reasons why you might want to do this, as you'll see throughout the examples in this chapter.

There's only one conceptual leap that you need to make, and it's a pretty small one at that. You just need to realize that JavaScript variables are generally created and set within <script> blocks in an HTML page. Since those <script> blocks can be generated dynamically with ColdFusion (just like any other portion of an HTML page), all you need to do is to output the values of your CFML variables in the correct spots, generally right after the JavaScript var keyword.

Passing Numbers to JavaScript

Let's take a look at a bare-bones variable passing scenario. First, take a look at the following lines of code, which creates a JavaScript variable called userInterestRate, available to any other JavaScript code in the same page:

 <script language="JavaScript1.1" type="text/javascript">  var userInterestRate = 3.95; </script> 

As you can see, the initial value of the variable will always be 3.95. If you wanted to change the initial value of the variable, you would need to edit the page and change the number, right? If you were just using static HTML pages, the answer is yes. But if you're using ColdFusion, you can just output a CFML variable in place of the 3.95, like so:

 <cfoutput>  <script language="JavaScript1.1" type="text/javascript">  var userInterestRate = #SESSION.MyInterestRate#;  </script> </cfoutput> 

Or, if you prefer:

 <script language="JavaScript1.1" type="text/javascript">  var userInterestRate = <cfoutput>#SESSION.MyInterestRate#</cfoutput>; </script> 

In either case, the actual value of the ColdFusion variable called SESSION.MyInterestRate will be inserted into the <script> block as the HTML code for the page is being sent back to the browser. If the value of SESSION.MyInterestRate is 5.32, say, then the browser will receive this <script> block:

 <script language="JavaScript1.1" type="text/javascript">  var userInterestRate = 5.32; </script> 

The browser's JavaScript interpreter will create the userInterestRate variable with the appropriate value. The browser doesn't know that the variable is being passed from ColdFusion; as far as the browser is concerned, there is no difference between this <script> block and the first snippet in this section.

One more thing, while we're on the subject. If the #SESSION.MyInterestRate# came from a database and the value in the database is currently NULL, ColdFusion will generate an empty string in place of the variable when the page is generated. That means the browser will receive this, which won't make any sense to the JavaScript interpreter and will thus cause an error message to be displayed:

 <script language="JavaScript1.1" type="text/javascript">  var userInterestRate = ; </script> 

There are two easy workarounds. The first is to use ColdFusion's Val() function around the variable, as in #Val(SESSION.MyInterestRate)#. This will cause ColdFusion to send a value of 0 to the browser when the actual value of the CFML variable is empty or null. That's a fine solution as long as the nature of your application doesn't demand that your scripts know the difference between a zero and a null. The slightly more sophisticated workaround is to send a proper JavaScript value of null when the CFML variable is empty, like so:

 <cfoutput>  <script language="JavaScript1.1" type="text/javascript">  <cfif SESSION.MyInterestRate EQ "">  var userInterestRate = null;  <cfelse>  var userInterestRate = #SESSION.MyInterestRate#;  </cfif>  </script> </cfoutput> 

Passing Strings to JavaScript

The process is only a tiny bit more complicated if you want to pass a string value instead of a number. To create a string variable in JavaScript, you use double or single quotes quotation marks to indicate the beginning and end of the string, much like you do in a <cfset> tag. For instance, the next snippet, if included in a static HTML page, would create an additional userName variable, with the value of Belinda Foxile:

 <script language="JavaScript1.1" type="text/javascript"> var userInterestRate = 3.95; var userName = "Belinda Foxile"; </script> 

So, if you wanted to populate the userName variable on the fly, you just refer to your ColdFusion variable between the quote quotation marks, like so:

 <cfoutput>  <script language="JavaScript1.1" type="text/javascript">  var userInterestRate = #SESSION.MyInterestRate#;  var userName = "#SESSION.MyUserName#";  </script> </cfoutput> 

This is all well and good, unless the ColdFusion variables contain characters that will confuse the JavaScript interpreter. For instance, if the value of the SESSION.MyUserName variable itself contains any quotation marks, like Belinda "Red" Foxile, the <script> block that gets sent to the browser is going to look like this:

 <script language="JavaScript1.1" type="text/javascript"> var userInterestRate = 3.95; var userName = "Belinda "Red" Foxile"; </script> 

Understandably, this is going to confuse the JavaScript interpreter. As far as it can tell, the string seems to end after the space that follows the Belinda part. It then doesn't know what to do with the part that begins with Red. The embedded quote characters need to be escaped properly so that JavaScript knows that they are meant to be considered part of the string, rather than indicating the end of the string. As a result, JavaScript will display an error message or do some other nasty thing when the page is loaded in a browser.

The solution is to use ColdFusion's excellent and handy JSStringFormat() function, which escapes the quote quotation marks for you. It also takes care of any other characters that need to be escaped in JavaScript string literals. JSStringFormat(), the string you want to pass to JavaScript. No matter what the string contains, the JSStringFormat() function It returns the properly escaped string. So, the CFML snippet to create the userName variable would become:

 <cfoutput>  <script language="JavaScript1.1">  var userInterestRate = #SESSION.MyInterestRate#;  var userName = "#JSStringFormat(SESSION.MyUserName)#";  </script> </cfoutput> 

The browser will receive the following. E, with each quote quotation mark preceded by a backslash character, which is the method of escaping special characters in JavaScript:

 <script language="JavaScript1.1"> var userInterestRate = 3.95; var userName = "Belinda \"Red\" Foxile"; </script> 

Table 17.11 shows a list of characters that JSStringFormat() automatically escapes for you. The result is a string that can safely be included in a page as a JavaScript literal (that is, between quote quotation marks) without problems.

Table 17.11. Characters Automatically Escaped by JSStringFormat()

CHARACTER

ESCAPED SEQUENCE

Backslash

\\

Carriage return

\r

New line

\n

Quote Quotation mark (double)

\"

Quote Quotation mark (single)

\'

Tab

\t


Variable Passing Example: Mortgage Calculator

Listing 17.1 shows how to create a JavaScript-powered page that performs computations based on variables passed from ColdFusion. When the page first appears, it asks the user to choose one of three mortgage programs: Low, Medium, or High interest (Figure 17.1). When the user selects a rate, the mortgage calculator appears, where the user can make computations based on the selected interest rate.

Figure 17.1. First, the user must choose a mortgage rate to pass to the calculator.


In addition to passing variables, this listing also demonstrates how to work with form fields, and how to execute a function when an element is changed or clicked.

Listing 17.1. MortgageCalculator.cfmPassing Variables to JavaScript
 <!--- Name:        MortgageCalculator.cfm Author:      Nate Weiss and Ben Forta Description: Demonstrates passing CFML variables              to a JavaScript-powered page. Created:     02/01/05 ---> <!--- User can select between several different interest types ---> <cfparam name="SESSION.MyInterestType"          type="string"          default="high"> <!--- If user is changing the interest type ---> <cfif IsDefined("URL.InterestType")>   <cfset SESSION.MyInterestType=URL.InterestType> </cfif> <!--- Assign an actual interest rate, based on the type ---> <!--- (in a real application, this would probably come from a database) ---> <cfswitch expression="#SESSION.MyInterestType#">   <cfcase value="High">     <cfset SESSION.MyInterestRate=8.53>   </cfcase>   <cfcase value="Medium">     <cfset SESSION.MyInterestRate=5.12>   </cfcase>   <cfcase value="Low">     <cfset SESSION.MyInterestRate=2.41>   </cfcase>   <cfdefaultcase>     <cfset SESSION.MyInterestType="">   </cfdefaultcase> </cfswitch> <!--- Pass the interest rate and type to JavaScript ---> <cfoutput>   <script type="text/javascript"           language="JavaScript">     var userInterestRate=#SESSION.MyInterestRate#;   </script> </cfoutput> <!--- Use dollarFormat function defined in separate file ---> <script type="text/javascript"         language="JavaScript"         src="/books/2/449/1/html/2/dollarFormat.js"></script> <script type="text/javascript"         language="JavaScript">   // Simple mortgage calculation function   // ...this probably isn't how real banks do it...  :)   function calcMortgage(amount, rate, years) {     var result=amount;     // Number of months     var months=years * 12;     // Actual mortgage calculation would go here     for (var month=0; month <= months; month++) {       var thisMonthsInterest=(result / months) * (rate / 12);       result=result + thisMonthsInterest;     }     return result;   }   // Wrapper function that performs calcMortgage() based on form input   // Also validates the form entries   function calcMortgageBasedOnForm() {     var lendAmount, numYears, computedResult;     var numYears;     // Get the amount of the mortgage from the form     lendAmount=parseFloat(document.forms[0].LendAmount.value);     // Get the number of years from the form     for (var i=0; i < document.forms[0].NumYears.length; i++) {       if (document.forms[0].NumYears[i].checked) {         numYears=parseInt(document.forms[0].NumYears[i].value);         break;       }     };     // Validation: if the number of years and lending amount have been supplied     if ( (numYears > 0) && (lendAmount > 0) ) {       // Call calcMortgage() function to get the mortgage cost       computedResult=calcMortgage(lendAmount, userInterestRate, numYears);       // Display the computed result to the user       document.forms[0].ComputedResult.value=dollarFormat(computedResult);     // If the years or lending amount is blank, clear the computed result     } else {       document.forms[0].ComputedResult.value="";     };   } </script> <html> <head><title>Mortgage Calculator</title></head> <body>   <h2>&quot;Fantasy&quot; Mortgage Calculator</h2>   <!--- Allow user to choose mortgage program --->   <cfif SESSION.MyInterestType EQ "">     Choose a mortgage program:<br>     <!--- For each mortgage program... --->     <cfloop list="Low,Medium,High"             index="This">       <!--- Provide a link to choose the program --->       <cfoutput>         <a href="MortgageCalculator.cfm?InterestType=#This#">#This#</a><br>       </cfoutput>     </cfloop>   <cfelse>     <!--- Use a form to hold the <input> elements --->     <!--- The "return false" keeps the form from being submitted --->     <form onsubmit="return false">       <!--- Text input for lending amount --->       <p><strong>Amount You Want to Borrow:</strong><br>       <input type="Text"              name="LendAmount"              size="12"              maxlength="10"              onchange="calcMortgageBasedOnForm()"              onkeyup="calcMortgageBasedOnForm()">       <!--- Radio buttons for length of mortgage --->       <p><strong>Mortgage Length:</strong><br>       <input type="Radio"              name="NumYears"              value="7"              onclick="calcMortgageBasedOnForm()">7 years       <br>       <input type="Radio"              name="NumYears"              value="15"              onclick="calcMortgageBasedOnForm()">15 years       <br>       <input type="Radio"              name="NumYears"              value="30"              onclick="calcMortgageBasedOnForm()">30 years       <br>       <!--- Disabled text field for displaying the computed result --->       <p>Computed Mortgage Cost:<br>       <input type="Text"              name="ComputedResult"              size="10"              readonly              style="background:lightgrey">     </form>     <!--- Link to choose a different rate --->     <p>     <a href="MortgageCalculator.cfm?InterestType=">     Choose different rate</a>     <br>   </cfif> </body> </html> 

The first portion of this listing deals with determining a numeric interest rate based upon the rate program (Low, Medium, or High) that the user selects. If the user chooses Low, the interest rate will be 0.241; if they choose High, it will be 0.853. Two SESSION variables, MyInterestType and MyInterestRate, are maintained to remember the user's selection. The variables are passed to JavaScript in the first <script> block.

Next, an external JavaScript file called dollarFormat.js is included with a <script src> element. This file creates a function called dollarFormat() which behaves like the CFML DollarFormat() function (you'll see the function later in Listing 17.2).

Next, a simple function called calcMortgage() is created, which accepts three arguments: amount, rate, and years. This author is a bit ashamed to say that he doesn't know how mortgages are actually calculated, but that's not really so important here (surely it's some kind of computation based on the mortgage amount, the interest rate, and the length of time the money will be borrowed). This function simply loops through each month of the mortgage period, adding the current month's interest to the result variable. When the loop is finished, the mortgage cost is considered to have been computed and is returned as the function's output.

With the calcMortgage() function in place, the next thing to do is to compute mortgages based on the user's form entries. The calcMortgageBasedOnForm() function takes care of this task. Conceptually, it's a wrapper around the calcMortgage() function. It simply gets the appropriate values from the form elements and passes them to calcMortgage(). Getting the lendAmount value is easy, since the amount is provided in a simple form field; the only twist is the addition of parseFloat() to convert the user's text entry into a floating-point number. Getting the numYears value is slightly more complicated, because it is necessary to loop through the NumYears array (which represents the radio buttons shown in Figure 17.2) to find which radio button the user selected.

Figure 17.2. Users can make computations based on the interest rate that was passed from ColdFusion.


After the for loop, the value of lendAmount will be a number, unless the form field is blank or if user entered something that couldn't be converted to a number (in which case lendAmount will hold the special JavaScript value of NaN, which means "not a number"). The value of numYears will hold an integer, unless none of the radio buttons have been selected (in which case it will hold a value of null, since no other value has been assigned to it).

If both values are greater than zero, the mortgage is calculated using the calcMortgage() function. The computed value is placed into the visual ComputedResult form field (which has its readonly flag set and is colored gray to indicate that the user won't be able to edit the field), formatting it with dollarFormat() along the way. If one or both of the input values is not provided, the ComputedResult field is cleared.

Within the <form> block itself, the onchange, onkeyup, and onclick events of the various form controls are set to execute the calcMortgageBasedOnForm() function whenever the user makes a change on the form.

The result is a small JavaScript application that receives a variable from ColdFusion's SESSION scope (in this case, the appropriate interest rate), then allows the user to work with the variable interactively. In this case, there is only one variable being passed to JavaScript, but you could easily pass multiple values using the same basic technique. Clearly, the value of the variable could come from a database query or other server-side source.

Listing 17.2 shows the JavaScript code used to create the dollarFormat() function. This is just one approach; there are many other ways in which this function could be written.

NOTE

In a situation such as this (when a common utility function is needed that is not available as a built-in method), I would recommend running a few searches at sites like www.javascript.com, www.webreference.com, or groups.google.com. Someone else is likely to have solved the problem before.


Listing 17.2. dollarFormat.jsA JavaScript Function for Formatting Numbers
 /*   Filename: dollarFormat.js   Author:   Nate Weiss (NMW)   Purpose:  Mimics the CFML DollarFormat() function   Note     With IE, you could just use num.toLocaleString() instead */ // Utility function that formats a number to a "dollar format" function dollarFormat(num) {  var arParts, dollarPart, centPart  // Split number based on the position of the period character, if any  arParts = num.toString().split(".");  // The dollar part is the part before the period  dollarPart = arParts[0];  // If there is a cent portion of the number, use it, otherwise use "00"  if (arParts.length > 1) {  centPart = (arParts[1] + "00").substr(0,2);  } else {  centPart = "00";  }  // Reset arParts to an empty array  arParts = new Array();  // Number of digits before first comma  // (but zero if there will be 3 digits before first comma)  var offset = dollarPart.length % 3;  // If there should be some digits (other than 3) before first comma,  // add an element to the array with that number of digits in it  if (offset > 0) {  arParts[0] = dollarPart.substr(0, offset);  }  // For all remaining groups of three digits, add additional  // elements to the array with the next 3 digits in it  for (i = offset; i < dollarPart.length; i = i + 3) {  arParts[arParts.length] = dollarPart.substr(i, 3);  };  // Join the array back into a string, separated by commas  dollarPart = arParts.join(",")  // Return the various parts, concatenated together  return "$" + dollarPart + "." + centPart; }; 

Passing Arrays to JavaScript

So far, you have only learned how to pass simple values from ColdFusion to JavaScript. Passing multifaceted data, like arrays and structures, can be nearly as straightforward. It can also get complicated. Later in this chapter, you will learn how to use the <cfwddx> tag to pass any type of variable to JavaScript, no matter how complex.

Let's start simple. You can pass arrays of numbers to JavaScript using the ArrayToList() function (assuming the CFML array is one-dimensional), like so:

 <cfoutput>  <script type="text/javascript" language="JavaScript1.1">  var userInterestRate = #SESSION.MyInterestRate#;  var userName = "#SESSION.MyUserName#";  var primeArray = new Array(#ArrayToList(MyPrimeNumberArray)#);  </script> </cfoutput> 

When this code is received by the browser, it will look like this, assuming that the ColdFusion MyPrimeNumberArray array has already been populated with the first few prime numbers:

 <script type="text/javascript" language="JavaScript1.1"> var userInterestRate = 3.95; var userName = "Belinda \"Red\" Foxile"; var primeArray = new Array(1,3,5,7,9,11,13); </script> 

NOTE

It's worth pointing out that JavaScript arrays start at 0 (instead of 1, as in ColdFusion). So, in your JavaScript code, you would refer to the first element of the array as primeArray[0], the second element as primeArray[1], and so on.


If you want to pass an array of strings, you can use the ListQualify() function. For instance:

 <cfoutput>  <script language="JavaScript1.1">  var userInterestRate = #SESSION.MyInterestRate#;  var userName = "#SESSION.MyUserName#";  var nameArray = new Array(#ListQualify(ArrayToList(NameArray), """")#);  </script> </cfoutput> 

If the strings in the array might contain quote quotation marks or the other special characters listed in Table 17.11, you could add JSStringFormat() around the ArrayToList() part (but inside the ListQualify() part).

But even this won't be perfect, because if the array contains any empty strings, those array elements will be missing from the JavaScript array, due to the way ColdFusion's string functions deal with consecutive delimiter characters. You can solve this problem by looping through the array, populating each of its elements manually, like so:

 <cfoutput>  <script language="JavaScript1.1">  var userInterestRate = #SESSION.MyInterestRate#;  var userName = "#SESSION.MyUserName#";  var nameArray = new Array();  <!--- Populate the nameArray variable --->  <cfloop from="1" to="#ArrayLen(CFMLNameArray)#" index="i">  nameArray[#Val(i - 1)#] = "#JSStringFormat(CFMLNameArray[i])#";  </cfloop>  </script> </cfoutput> 

The browser would receive the following (whitespace notwithstanding):

 <script type="text/javascript" language="JavaScript1.1"> var userInterestRate = 3.95; var userName = "Belinda \"Red\" Foxile"; var nameArray = new Array(); nameArray[0] = 1; nameArray[1] = 3; nameArray[2] = 5; nameArray[3] = 7; nameArray[4] = 9; nameArray[5] = 11; nameArray[6] = 13; </script> 

When dealing with a potentially complex object such as an array (which could contain other arrays, or structures, or record sets), it is usually a lot easier to simply use the <cfwddx> tag as shown below. This will automatically generate JavaScript code that is functionally equivalent to the snippet shown above:, like so:

 <cfoutput>  <script language="JavaScript1.1">  var userInterestRate = #SESSION.MyInterestRate#;  var userName = "#SESSION.MyUserName#";  var <cfwddx action="CFML2JS"              input="#CFMLNameArray#"              toplevelvariable="nameArray">  </script> </cfoutput> 

Contrary to the way you learned to use <cfwddx> in Chapter 16, "Using WDDX", this form of <cfwddx> doesn't have anything to do with WDDX packets or XML. See the "Using JavaScript with WDDX" section at the end of this chapter for details about using <cfwddx> in this fashion.

Passing Structures to JavaScript as Objects

The JavaScript Object data type corresponds fairly closely to ColdFusion's structure type. If you have a CFML structure called MyStruct which contains simple string data, you could use the following loop to re-create the corresponding object in JavaScript:

 <cfoutput>  <script language="JavaScript1.1">  var userInterestRate = #SESSION.MyInterestRate#;  var userName = "#SESSION.MyUserName#";  var myObject = new Object();  <!--- Populate the myObject variable --->  <cfloop collection="#MyStruct#" item="ThisKey">  myObject["#ThisKey#"] = "#JSStringFormat(MyStruct[ThisKey])#";  </cfloop>  </script> </cfoutput> 

NOTE

Again, you could do this with one step using <cfwddx> as discussed in the latter portion of this chapter. I'm showing how to do it the "manual" way so that you get a sense of how Objects and Structures are related conceptually.


NOTE

Again, you could do this with one step using <cfwddx> as discussed in the latter portion of this chapter.


For instance, if MyStruct contains three values for Belinda, Ben, and Nate, the resulting <script> block that gets sent to the browser might look like this:

 <script language="JavaScript1.1"> var userInterestRate = 3.95; var userName = "Belinda \"Red\" Foxile"; var myObject = new Object(); myObject["Belinda"] = "Teen Pop Superstar"; myObject["Ben"] = "ColdFusion Superstar"; myObject["Nate"] = "Belinda's Biggest Fan"; </script> 

Passing Enough Data to Relate Two Select Boxes

One JavaScript-powered trick that can be effective in ColdFusion applications is the notion of "cascading" select lists. A long time ago now, this author wrote a simple CFML Custom Tag called <CF_TwoSelectsRelated>, which causes two drop-down lists to appear on the page, filled with data from a query. When the user chooses an option in the first select list, the second select list fills with the appropriate data, without having to reload the page.

I continue to be surprised by the number of people who use this custom tag. Many people write to me asking for one enhancement or modification or another, thinking that it would be really hard to make the changes themselves. Examining a few different approaches to connecting the select lists will be an interesting way to learn more about how to pass complex, multifaceted data from ColdFusion to JavaScript, and how the data can be used in JavaScript once it gets there. I continue to be surprised by the number of people who use this custom tag.

Examining a few different approaches to connecting the select lists will be an interesting way to learn more about how to pass complex, multifaceted data from ColdFusion to JavaScript, and how the data can be used in JavaScript once it gets there. Hopefully, the discussion will also serve to demystify what it actually takes to create interactive pages that use ColdFusion and JavaScript together.

Cascading Selects, Approach #1: Creating an Array of Films

Listing 17.3 shows one approach to solving this problem. The page contains a single form with two select lists: one for ratings, and the other for corresponding films. When the page first appears, no rating is selected and no films are displayed (Figure 17.3). When the user selects a rating from the first list, the films that match that rating magically appear in the second list (Figure 17.4). People love this because it makes really efficient use of real estate, and because the "lookup" operation executes more or less instantly, without the need for a page refresh.

Listing 17.3. JSRelatedSelects1.cfmUsing a Loop to Create an Array of Films on the Fly
 <!--- Name:        JSRelatedSelects1.cfm Author:      Nate Weiss and Ben Forta Description: Demonstrates one approach to relating              select lists via JavaScript, using              film and rating data from the ows              example database. Created:     02/01/05 ---> <!--- Get film data from database ---> <cfquery name="FilmsQuery"          datasource="ows">   select FilmID, MovieTitle, RatingID   FROM Films   WHERE RatingID IS NOT NULL   ORDER BY RatingID, MovieTitle </cfquery> <!--- Get rating data from database ---> <cfquery name="RatingsQuery"          datasource="ows">   select RatingID, Rating   FROM FilmsRatings   ORDER BY RatingID </cfquery> <html> <head> <title>Cascading Select Lists</title> <!--- Custom JavaScript code ---> <script type="text/javascript" language="JavaScript">   // Create a new array to hold film "objects"   var arFilms=new Array;   <!--- For each film... --->   <cfoutput query="FilmsQuery">     // Create a JavaScript object for this film     var oFilm=new Object;     oFilm.filmid=#FilmsQuery.FilmID#;     oFilm.ratingid=#FilmsQuery.RatingID#;     oFilm.movieTitle="#JSStringFormat(MovieTitle)#";     // Append the object to the end of the array of films     arFilms[arFilms.length]=oFilm;   </cfoutput>   // This function fills the second select box based on the first box's value   function fillFilms() {     // Get the currently selected rating ID from the first select box     with (document.forms[0].RatingID) {       var ratingid=options[selectedIndex].value;     }     // Stop here if there is no selected rating     if (ratingid == null) {       return;     }     // Remove all options from the second select box     document.FilmForm.FilmID.options.length=0;     // For each item in the films array...     for (var i=0; i < arFilms.length; i++) {       // If the film's rating is the same as the currently selected rating...       if (arFilms[i].ratingid == ratingid) {         // Create a new visual <option> to place in the second select box         var objOption=new Option(arFilms[i].movieTitle, arFilms[i].filmID);         // Place the new option in the second select box         with (document.FilmForm.FilmID) {           options[options.length]=objOption;         }       }     };   }; </script> </head> <body> <!--- Ordinary html form ---> <form action="ShowFilm.cfm"       name="FilmForm"       method="Post">   <!--- First select box (displays ratings) --->   <strong>Rating:</strong><br>   <select name="RatingID" onchange="fillFilms()">     <option>[please choose a rating]     <cfoutput query="RatingsQuery">       <option value="#RatingID#">#Rating#     </cfoutput>   </select>   <!--- Second select box (displays films) --->   <p><strong>Film:</strong><br>   <select name="FilmID" size="5">     <option>[choose a rating first]   </select><br> </form> </body> </html> 

Figure 17.3. Users can choose ratings from the first select list to view matching films.


Figure 17.4. When a rating is selected, corresponding films appear in the second list.


Let's start with the <form> portion of this listing, near the bottom. As you can see, this is a fairly ordinary HTML form, which uses normal <select> tags to create two select lists named RatingID and FilmID. A <cfoutput> block is used to fill the first list with options for each record in the Ratings table (from the ows example database). The second list is left empty, except for a single option that tells the user to choose a film first (see Figure 17.3). The only thing out of the ordinary is in the first <select> list: it contains an onchange attribute that tells JavaScript to execute a function called fillFilms() when the user makes a rating selection.

Now look at the <script> block at the top of the template. The first thing it does is to create a JavaScript array called arFilms. This array will be filled with JavaScript objects (which, remember, are similar to structures in CFML), where each object represents one film.

The next block uses <cfoutput> to loop over the film records in the FilmsQuery query. This is the part that creates an object for each film, appending each object to the arFilms array as it goes. This may look a bit confusing when you first see it, because JavaScript syntax and ColdFusion syntax appear to be co-mingled. The thing to keep in mind is that the ColdFusion syntax will execute on the server before the page is sent to the browser. The browser will only receive the remaining, dynamically generated JavaScript code. For instance, depending on the actual film information in the database, the browser will receive JavaScript code similar to the following (there will one such chunk for each film):

 // Create a JavaScript object for this film var oFilm = new Object; oFilm.filmid = 16; oFilm.ratingid = 1; oFilm.movieTitle = "West End Story"; // Append the object to the end of the array of films arFilms[arFilms.length] = oFilm; 

So, after the <cfoutput> block has finished its work and the browser's JavaScript interpreter parses the script code, the browser's memory will contain an arFilms array that contains the ID number, title, and rating for each film. The rating of the first film is available as arFilms[0].ratingid, the title of the second film is arFilms[1].movieTitle, and so on. The ID number of the last film in the array could be accessed as arFilms[arFilms.length].filmid (or, if you prefer, arFilms[arFilms.length] ["filmID"]).

The next part of the listing is the code for the fillForms() function (which executes when the user changes the selection in the first select list). First, the selected rating is obtained by getting the value attribute of the selected <option> from the <select> named ratingid. If no ratingid is currently selected, the function exits immediately.

Next, any existing options are removed from the second select list by setting the length of its options collection to 0. Then a for loop is used to iterate through the arFilms array; within the loop, the "current" film can be referred to as arFilms[i]. The if test checks to see if the current film's rating is the same as the currently selected ratingid. If so, a new Option object is created which displays the title of the film and has the film's ID number as its value. The new option is then added to the second select list by appending it to its options collection.

NOTE

Creating a new Option object is like creating an HTML <option> tag out of thin air. The first argument for Option's constructor is the text that should be displayed for the option; the second argument is the value of the option (what gets sent to the server when the form is submitted). Option objects are only meant to be used for <select> elements. Consult a JavaScript reference for all the gory details.


Cascading Selects, Approach #2: Using a Custom JavaScript Object

Listing 17.4 shows a slightly different approach to the same problem. Rather than filling the arFilms array with instances of the generic Object type (which is like a CFML structure), it fills the array with instances of a custom object type called Film. The page behaves the same way as the previous listing (see Figure 17.3 and Figure 17.4).

Listing 17.4. JSRelatedSelects2.cfmUsing Instances of Custom JavaScript Objects to Hold Data
 <!--- Name:        JSRelatedSelects2.cfm Author:      Nate Weiss and Ben Forta Description: Demonstrates one approach to relating              select lists via JavaScript, using              film and rating data from the ows              example database. Created:     02/01/05 ---> <!--- Get film data from database ---> <cfquery name="FilmsQuery"          datasource="ows">   select FilmID, MovieTitle, RatingID   FROM Films   WHERE RatingID IS NOT NULL   ORDER BY RatingID, MovieTitle </cfquery> <!--- Get rating data from database ---> <cfquery name="RatingsQuery"          datasource="ows">   select RatingID, Rating   FROM FilmsRatings   ORDER BY RatingID </cfquery> <html> <head> <title>Cascading Select Lists</title> <!--- Custom JavaScript code ---> <script type="text/javascript"         language="JavaScript">   // Create a new array to hold film "objects"   var arFilms=new Array;   // Define a custom JavaScript object type to represent a single film   function Film(filmID, ratingID, movieTitle) {     this.filmid=filmID;     this.ratingid=ratingID;     this.movieTitle=movieTitle;     this.makeOption=makeOption;   }   // This function becomes the makeOption() method of every Film object   function makeOption() {     return new Option(this.movieTitle, this.filmID);   };   <!--- For each film, append a new Film object to the array of films --->   <cfoutput query="FilmsQuery">     arFilms[arFilms.length]=       new Film(#FilmID#, #RatingID#, "#JSStringFormat(MovieTitle)#");   </cfoutput>   // This function fills the second select box based on the first box's value   function fillFilms() {     // Get the currently selected rating ID from the first select box     with (document.forms[0].RatingID) {       // Stop here if there is no selected rating       if (selectedIndex == -1) {         return;       }       var ratingid=options[selectedIndex].value;     }     // Remove all options from the second select box     document.FilmForm.FilmID.options.length=0;     // For each item in the films array...     for (var i=0; i < arFilms.length; i++) {       // If the film's rating is the same as the currently selected rating...       if (arFilms[i].ratingid == ratingid) {         // Place a new option in the second select box         with (document.FilmForm.FilmID) {           options[options.length]=arFilms[i].makeOption();         }       }     };   }; </script> </head> <!--- Call the fillFilms() function when the page first appears ---> <body onload="fillFilms()"> <!--- Ordinary html form ---> <cfform action="ShowFilm.cfm"         name="FilmForm"         method="Post">   <!--- First select box (displays ratings) --->   <strong>Rating:</strong><br>   <cfselect name="RatingID"             query="RatingsQuery"             value="RatingID"             display="Rating"             onchange="fillFilms()"/><br>   <!--- Second select box (displays films) --->   <p><strong>Film:</strong><br>   <cfselect name="FilmID"             size="5"             style="width:300px"/><br>   <!--- Submit button --->   <cfinput name="sbmt" type="Submit"> </cfform> </body> </html> 

The main addition in this listing is the Film() function, near the top of the <script> block. This function isn't meant to be called normally; it's meant to be called with the new keyword to create instances of meant to be used specially, to create a the custom object type. Unfortunately, there isn't space here to explain everything about creating custom objects in JavaScript. Here are the basics:

  • The idea is that any function can be called with the new keyword, which means "new instance of". Using object-oriented terminology, the function is now a constructor for a new object type, or class.

  • Functions that are called in this way can use the this keyword to track data about each instance of the object.

  • Arguments passed to the function can be stored in the this scope; these values will be stored separately for each instance of the object. Using object-oriented terminology, they are now the object's properties.

  • You can also add functions to a JavaScript object, in which case the functions can be called as methods of the object. The body of the method's code can also refer to the this scope to access or manipulate that object's instance level data.

NOTE

JavaScript's custom object types are similar conceptually to ColdFusion Components (CFCs) that hold instance-level data (as discussed in Chapter 19, "Creating Advanced ColdFusion Components"). You'll notice that both custom JavaScript objects and CFCs use the word this to represent the data tracked by each instance of the object being created.


So, the function Film() block in this listing creates a new object type called Film with filmID, ratingID, and movieTitle properties, and a single method called makeOption(). A new instance of the Film class can be created like so, if its ID number is 50 and its rating is 2:

 var myInstance = new Film(50, 2, "Stuart Spittle"); 

Once created, its properties can be accessed as myInstance.filmID and myInstance.movieTitle. In this example, the values of these properties never change; they simply retain the values provided when the instance is created.

The makeOption() method returns an Option object (as discussed in the text after Listing 17.3). Because this.filmID and this.movieTitle are used when creating the new Option, calling an instance's makeOption() method always creates an option that contains that instance's film data. Conceptually, each instance knows how to describe itself in the form of a visual drop-down option.

The makeOption() method can be called like so:

 var myNewOption = myInstance.makeOption(); 

Or, to create a new method and make it appear in a select list all at once, where element is a reference to the <select> element:

 element.options[element.options.length] = myInstance.makeOption(); 

With the new Film object type in place, the remaining changes between Listing 17.3 and Listing 17.4 are pretty straightforward. The <cfoutput query="FilmsQuery"> portion is now shorter, because it simply calls the Film object's constructor to create each object to store in the arFilms array. The last line of the <script> block, which creates the visual option for each film that matches the current rating selection, now calls the makeOption() method instead of creating an Option object on its own.

Again, the end user's experience is not any different from that of Listing 17.3. What this listing aims to demonstrate is how you can easily use JavaScript's object- oriented programming metaphors (such as this and new, and the concepts of classes and instances) even when the data is being "passed" from ColdFusion. This allows you to write JavaScript code that uses objects in much the same way that your ColdFusion code might use CFCs.

NOTE

This version of the page also uses <cfform> instead of <form> and <cfselect> instead of <select> to create the form itself. I made this change mainly to prove that the browser doesn't care which tags you use, since what it receives from ColdFusion contains the same HTML tags either way.


NOTE

This version of the page calls the fillFilms() function in the <body> tag's onload event, which means that the second list is filled right away when the form first appears.




Advanced Macromedia ColdFusion MX 7 Application Development
Advanced Macromedia ColdFusion MX 7 Application Development
ISBN: 0321292693
EAN: 2147483647
Year: 2006
Pages: 240
Authors: Ben Forta, et al

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net