Passing Data to JavaScript Using <cfwddx>So far in this chapter, you have seen how to pass variables from ColdFusion to JavaScript by generating the appropriate script code on the fly, using <cfoutput> and other basic CFML tags. The listings have worked out just fine; there is nothing wrong with this approach. For lack of a better term, I'm going to call this the roll-your-own approach to passing variables to JavaScript. ColdFusion provides a higher-level tool for passing variables to JavaScript: the CFML2JS action of the <cfwddx> tag. You were introduced to <cfwddx> in Chapter 16, "Using WDDX." That chapter explained how to use the tag to serialize and deserialize data in WDDX packets (a form of XML). This section will focus on a completely different use for <cfwddx>, which is passing variables to JavaScript. This use of <cfwddx> has nothing directly to do with WDDX packets or XML. While it is consistent with WDDX's overall mission (to make it really easy to transfer data from place to place), it is not really about the WDDX format per se. Instead of producing an XML version of a ColdFusion variable or value, this form of <cfwddx> produces a chunk of JavaScript code that, when executed by the browser's interpreter, re-creates the variable or value in the browser's memory. In other words, instead of converting your data into a WDDX packet, this method converts your data to a bunch of JavaScript code. Using this Form of <cfwddx>Table 17.12 shows the <cfwddx> syntax for sending data to JavaScript. As you can see, most of the attributes are common to the way the tag is used to produce WDDX packets (see Chapter 16).
So, to transfer a ColdFusion variable called MyValue to JavaScript, you use code similar to the following: <script language="JavaScript"> <cfwddx action="CFML2JS" input="#MyValue#" toplevelvariable="myValueFromCF"> </script> If the value of MyValue is a string, the code received by the browser will be similar to the following: <script language="JavaScript"> myValueFromCF = "Hello, World!"; </script> If MyValue holds an array, the code received by the browser might be similar to this (depending on the actual data in the array, of course): <script language="JavaScript"> myValueFromCF = new Array(); myValueFromCF[0] = "Belinda"; myValueFromCF[1] = "Ben"; myValueFromCF[0] = "Nate"; </script> Most commonly, the <cfwddx> tag is placed between <script> tags (as shown in the first snippet). Sometimes you may prefer to store the JavaScript code in a string variable, outputting it within a <script> tag later in your ColdFusion page. Just use the output attribute to hold the generated code, like so: <!--- Convert MyValue to JavaScript code ---> <cfwddx action="CFML2JS" input="#MyValue#" output="GeneratedJS" toplevelvariable="myValueFromCF"> <!--- Output generated JavaScript code ---> <script language="JavaScript"> <cfoutput>#GeneratedJS#</cfoutput> </script> NOTE If you were to output the value of GeneratedJS without surrounding it with <script> tags, the browser would just display the JavaScript code as text, rather than parsing and understanding the code. In any case, the value you supply to input can be arbitrarily complex; it could be a structure that contains numerous arrays, each of which holds an arbitrary number of smaller structures. In fact, you'll see that happen in the next section. NOTE If the value you supply to input contains a query recordset object (or objects), you must use an additional <script> tag to include the wddx.js file. The file teaches JavaScript how to deal with recordsets, something it doesn't understand out of the box. For details, please see the "Working with WddxRecordset Objects" section later in this chapter. Is This Serialization?I find it interesting to note that both types of output (the WDDX packet and the JavaScript code) are text-only representations of the original data. As such, both operations could be said to serialize the data, and the JavaScript interpreter on the browser machine could be said to deserialize the data. Think about the steps in a "normal" WDDX scenario, as you learned in Chapter 16:
Now think about the steps when using <cfwddx> in this new form:
The steps are pretty similar conceptually, apart form from the way the data looks while in its serialized state. The first scenario uses XML as the serialization format, and the second uses JavaScript code. Of course, the XML form has more uses, since it can be unpacked by any WDDX-aware application (or even just an XML-aware one). Just something to think about! Cascading Selects, Approach #3: Passing the Data Via <cfwddx>Listing 17.5 demonstrates a third approach to the cascading select boxes problem. This approach assembles a CFML structure of ratings and their corresponding films (the structure is assembled on the server). Then the <cfwddx> tag is used to "pass" the structure to JavaScript in a single step. The JavaScript code uses this data to populate the second select list. The end-user experience is once again the same (as shown in Figure 17.4 above). Listing 17.5. JSRelatedSelects3.cfmUsing <cfwddx> to Supply Data for the Second Select List<!--- Name: JSRelatedSelects3.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> <!--- Create a new structure to hold the ratings ---> <cfset RatingsStruct=StructNew()> <!--- For each rating returned by the database... ---> <cfloop query="RatingsQuery"> <!--- We are currently working with this film rating ---> <cfset ThisRatingid=RatingsQuery.RatingID> <!--- Create a new value in the structure, which is an array of films ---> <cfset RatingsStruct[RatingID]=ArrayNew(1)> <!--- Use an in-memory-query to get the films with this rating ---> <cfquery dbtype="query" name="MatchingFilms"> select * FROM FilmsQuery WHERE Ratingid=#ThisRatingID# </cfquery> <!--- For each matching film... ---> <cfloop query="MatchingFilms"> <!--- Create a new structure to hold the film's ID and title ---> <cfset FilmStruct=StructNew()> <cfset FilmStruct.filmid=MatchingFilms.FilmID> <cfset FilmStruct.movietitle=MatchingFilms.MovieTitle> <!--- Append the structure to the array for this rating ---> <cfset ArrayAppend(RatingsStruct[RatingID], FilmStruct)> </cfloop> </cfloop> <html> <head> <title>Cascading Select Lists</title> <!--- Custom JavaScript code ---> <script type="text/javascript" language="JavaScript"> <!--- Output the JavaScript code needed to create a JavaScript object ---> <!--- called objRatings that holds the same data as RatingsStruct ---> <cfwddx action="CFML2JS" input="#RatingsStruct#" toplevelvariable="objRatings"> // 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; // Grab the appropriate array of films from the objRatings object var arFilms=objRatings[ratingID]; // For each item in the films array... for (var i=0; i < arFilms.length; i++) { // Place a new option in the second select box with (document.FilmForm.FilmID) { options[options.length]= new Option(arFilms[i].movietitle, arFilms[i].filmid); } }; }; </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 type="Submit" name="sbmt"> </cfform> </body> </html> After the queries at the top of the page, a new CFML structure called RatingStruct is created. Then, within a <cfloop> that loops over each rating in the database, a new array is created with ArrayNew() and stored in RatingsStruct (using the rating ID as the name). Next, an in-memory-query is used to get the films for the current rating, and an inner <cfloop> is used to loop over each of the films. Within this inner loop, a structure is created to represent the film, holding the film's ID number and title. This film structure is then appended to the end of the array for the current rating. NOTE In other words, when the nested loops have finished executing, RatingStruct will contain an array of films for each rating. The arrays will be filled with smaller structures that contain the ID and title for each film. You could access the title of the second film with a rating of 5 using RatingStruct[5][2].MovieTitle. With the RatingsStruct structure in place, it can be passed to JavaScript in one step using the <cfwddx> tag. In this listing, the toplevelvariable attribute establishes that the data shall be known to JavaScript as an object named objRatings. If you use your browser's View Source command, you can see the JavaScript code that <cfwddx> generates to re-create the structure on the client. Here's an excerpt: objRatings = new Object(); objRatings["1"] = new Array(); objRatings["1"][0] = new Object(); objRatings["1"][0]["movietitle"] = "Charlie's Devils"; objRatings["1"][0]["filmid"] = 2; objRatings["1"][1] = new Object(); objRatings["1"][1]["movietitle"] = "Four Bar-Mitzvahs and a Circumcision"; objRatings["1"][1]["filmid"] = 4; ... objRatings["4"] = new Array(); objRatings["4"][0] = new Object(); objRatings["4"][0]["movietitle"] = "Ground Hog Day"; objRatings["4"][0]["filmid"] = 7; objRatings["4"][1] = new Object(); objRatings["4"][1]["movietitle"] = "Raiders of the Lost Aardvark"; objRatings["4"][1]["filmid"] = 17; objRatings["4"][2] = new Object(); objRatings["4"][2]["movietitle"] = "The Sixth Nonsense"; objRatings["4"][2]["filmid"] = 15; As you can see, there's no magic here. This JavaScript isn't any different conceptually from the JavaScript that the previous versions of this example generated. What's cool is that <cfwddx> did it all for us, automatically! The remainder of Listing 17.5 is very similar to the previous versions of this example (Listing 17.4 and Listing 17.3). Within this version's fillFilms() function, the JavaScript code is able to obtain an array of films for a particular rating using objRatings[ratingID]. This returns the JavaScript equivalent of the array that was created for the rating in the CFML portion of the listing. Because each element of the array contains an object that in turns holds filmid and movietitle properties. NOTE Because ColdFusion is not case-sensitive (but JavaScript is), structure key names are always re-created in JavaScript using lower case. That's why the JavaScript portion of the code must refer to arFilms[i].movietitle instead of arFilms[i]. MovieTitle or arFilms[i].movieTitle. Whenever you use <CFWDDX> to pass structures to JavaScript, the resulting JavaScript objects will use lower case for the property names. For this reason, I recommend that you use lowercase names when building the corresponding CFML structures (like this listing does when assigning values to FilmStruct). Otherwise, it can be confusing to use one spelling in the CFML portion of your code and the all-lowercase spelling in the JavaScript portion. Working with WddxRecordset ObjectsMost ColdFusion data types correspond to JavaScript data types rather neatly (strings become strings in JavaScript, dates become Date objects, arrays become Array objects, and so on). However, the ColdFusion notion of a recordset (or query object) has no obvious counterpart in JavaScript. For this reason, a simple, lightweight implementation of a JavaScript-based recordset object is included with ColdFusion. The object is called WddxRecordset. Whenever you use <cfwddx> to pass a query object to JavaScript, the generated JavaScript code will construct a new instance of this object. Including the wddx.js JavaScript FileThe WddxRecordset object is implemented in a file called wddx.js, which is automatically installed when you install ColdFusion. You must include the file with a set of <script> tags as discussed in this section. Otherwise, the browser will not understand what a WddxRecordset is, and you will likely see an error message reporting that "WddxRecordset is undefined" or something similar. By default, the file is located in the CFIDE/scripts folder within your Web server's document root. This means that you can use a line like the following to properly include the wddx.js file: <!--- Include wddx.js, located in the /CFIDE/scripts folder ---> <script language="JavaScript" src="/books/2/449/1/html/2//CFIDE/scripts/wddx.js"></script> If the wddx.js file has been moved or deleted, or if you are using a virtual Web server instance that has a different document root, that relative URL may not be valid. If so, one option would be to configure your Web server such that the /CFIDE/scripts prefix maps to the folder that actually contains wddx.js. Or, more simply, you can just copy the wddx.js file to the same folder as the ColdFusion pages you need to use it in, adjusting the relative path accordingly, like so: <!--- Include wddx.js, located in this folder ---> <script language="JavaScript" src="/books/2/449/1/html/2/wddx.js"></script> Simply to ensure that all the examples work correctly without adjustments, the example listings for this chapter use this method. Just do whatever makes sense for your situation. NOTE Conceptually, this line means the same thing to the browser as that a <cfinclude> tag means to ColdFusion. The browser will fetch the file over the Internet and execute its code inline, as if it appeared between the <script> tags. Using WddxRecordset MethodsThe WddxRecordset object supports a number of methods for getting data in and out of the recordset, as listed in Table 17.13. If you read Chapter 16, you may notice that the nature and scope of these methods are similar to the ones provided for the COM and Java implementations of WDDX that were discussed in that chapter. The theory in all these cases is the same: to provide a basic notion of a recordset that includes just enough methods to make the recordset useful, while keeping it easy to use, understand, and support.
NOTE Complete reference information and further examples are provided in the WDDX Software Development Kit (SDK), which is available from http://www.openwddx.org. NOTE For all methods that take a col argument, the column name is not case sensitive (the column names are stored internally in lowercase to achieve the case-insensitivity). The only exception is if you create a new recordset from scratch on the client as discussed in the Creating Recordsets from Scratch sidebar for details, in which case you can specify that the case of the column names are preserved (details in the WDDX SDK). Otherwise, the capitalization of recordset column names is not considered to be important. You'll see the getrowCount() and getField() methods used in the next code listing. Cascading Selects, Approach #4: Using a WddxRecordset ObjectAs an example of how to use WddxRecordset in an actual Web page, let's return to the cascading select problem. Listing 17.6 shows a fourth solution to the problem, this time using <cfwddx> to send the FilmsQuery recordset to the browser as a variable called rsFilms. Once interpreted by the browser's script engine, the rsFilms object will be an instance of WddxRecordset, meaning that any of the methods listed in Table 17.13 can be used to retrieve (or change) the data it contains. Listing 17.6. JSRelatedSelects4.cfmPassing Recordset Data to Relate the Two Select Lists<!--- Name: JSRelatedSelects4.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> <!--- Include the wddx.js file (in same folder as this ColdFusion page) ---> <!--- This allows us to receive and work with WddxRecordset objects ---> <script type="text/javascript" src="/books/2/449/1/html/2/wddx.js" language="JavaScript"></script> <!--- Custom JavaScript code ---> <script type="text/javascript" language="JavaScript"> <!--- Output the JavaScript code needed to create a JavaScript object ---> <!--- called objRatings that holds the same data as RatingsStruct ---> <cfwddx action="CFML2JS" input="#FilmsQuery#" toplevelvariable="rsFilms"> // 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 row=0; row < rsFilms.getRowCount(); row++) { // If this is a matching film if ( rsFilms.getField(row, "RatingID") == ratingID ) { // Place a new option in the second select box with (document.FilmForm.FilmID) { options[options.length]=new Option( rsFilms.getField(row, "MovieTitle"), rsFilms.getField(row, "FilmID")); } }; }; }; </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 type="Submit" name="sbmt"> </cfform> </body> </html> If you use the number of lines of code as a measure of simplicity, this is the simplest approach yet. Whether it actually feels simpler to you as a developer is a matter of perspective and personal preference. As you can see, this listing is structurally similar to the versions that came before it. The important differences introduced in this version are as follows:
Creating Recordsets from ScratchMost of the time, you will receive a WddxRecordset from ColdFusion as a result of the <cfwddx> tag, or by deserializing a WDDX packet that contains a <recordset> block. That said, you may occasionally need to create a WddxRecordset from scratch in your JavaScript code. Just create a new recordset with new WddxRecordset(), then use the addColumn(), addRows(), and setField() methods from Table 17.12. For instance, you could create a new recordset with JavaScript code like the following: rs = WddxRecordset() rs.addColumn("firstname") rs.addColumn("lastname") rs.addRows(2); rs.setField(0, "firstname", "Nate"); rs.setField(0, "lastname", "Weiss"); rs.setField(1, "firstname", "Winona"); rs.setField(1, "lastname", "Ryder"); Conceptually, the steps are similar to the QueryNew(), QueryAddRow(), QueryAddColumn(), and QuerySetCell() functions in CFML. You can also pass the initial column names and number of rows to the WddxRecordset() constructor; see the WDDX SDK for details. |