Creating an Advanced Custom Tag


You're going to have to hold on tight for this ride because it's complicated at times, but it's worth the trip. Make notes in the margins of the book, build and run the code yourself as you read along, make a strong pot of coffeewhatever it takes to stay on point. When you're done, follow my instructions at the end of this chapter to expand on what you've learned, and you'll be well on your way to building complete libraries of custom tags that can be reused between applications with little or no modification.

We're going to build another pair of nested custom tags that create a form. This form is going to actually manipulate data in a database and will adapt to being an Add form, an Edit form, a View form, or a Delete form. You'll leverage the bidirectional communication mechanism you just examined; you'll incorporate the flow control techniques you learned about in "Using CFEXIT to Control Custom Tag Processing Flow"; and you'll incorporate your own custom formStatus property into the custom tags so you can precisely control how and when the form is prepared and built.

Two Phases: Initial Page Display and PostBack

This form will post to itself so that all its functionality can be contained in the custom tags rather than split into a separate action page. To make this work, we'll need to split page processing into two phases: Initial Page Display, and the PostBack that occurs when the form posts back to itself. We have to know when these phases occur because the custom tags will need to display the form during the Initial Page Display and update the database during the PostBack phase.

Figure 18.18 shows the process flows that occur during the Initial Page Display phase that presents the form to the user. The child tags don't have Inactive or End modes because they are not implemented as paired custom tags.

Figure 18.18. Processing flow of the Initial Page Display of a family of advanced nested custom tags.


Figure 18.19 shows the process flows that occur during the PostBack phase that manipulates the database using the data submitted from the form.

Figure 18.19. Processing flow of the PostBack of a family of advanced nested custom tags.


Now that you understand the Initial Page Display and PostBack phases, let's look at how we'll control the Preparing and Building phases for the form.

Two Statuses: Preparing and Building

Think for a moment about displaying an Edit form populated with a record to be edited in the form. The first thing we have to do is determine which columns of data will populate the Edit form, and then we have to retrieve the record. All this work must be prepared before we even begin building the form itself, and we'll need information from the child tags in order to do it. Once the Preparing phase is finished, we go into the Building phase and build the form.

Using CFEXIT to Control Child Tag Processing Flow

Remember that <cfexit method="Loop"> jumps back into the tag's Inactive mode, which in turn passes control to the first child tag nested in the body of the parent tag. We're going to leverage this by calling <cfexit method="Loop"> as soon as we enter the building phase, so that the child tags can rerun and begin outputting fields into the parent form. (The first time we run the child tags, we're just getting their data so we can assemble an SQL statement to retrieve the record; the second time we run the child tags we're outputting the form fields themselves.)

If this all sounds confusing, refer back to Figure 18.18 to get a feel for how all these mechanisms are going to work together. Building the tags step-by-step will also help you understand how it all works, so let's begin.

Creating an Add Form and a Form Field

We're going to build this form in layers so that we can concentrate on one piece of functionality at a time and not get overwhelmed. We'll start by building the Add form functionality first.

First, though, let's create the list page in Listing 18.13. This will display the contents of the Company table and provide links to the form pages we'll build using the cf_DisplayForm and cf_DisplayField custom tags. For the sake of simplicity, create all your files in the Chapter18 directory you setup for this chapter.

NOTE

The script to create the database for this chapter is in the Chapter18 directory of this book's CD-ROM. Use it to create a database named Chapter18, and then create a ColdFusion data source named Chapter18 that connects to this database.


Listing 18.13. CompanyList.cfmCreating a List of Companies
 <!--- Author: Adam Phillip Churvis -- ProductivityEnhancement.com ---> <cfquery name="companies" datasource="#Application.dbDSN#">   SELECT CompanyID, CompanyName, ZipCode FROM Company </cfquery> <cfif IsDefined("URL.message")>   <cfoutput>#URL.message#</cfoutput> </cfif> <table cellspacing="2" cellpadding="2" border="1"> <tr>   <td><b>Company Name</b></td>   <td><b>Zip Code</b></td>   <td>[<a href="CompanyAddForm.cfm">Add</a>]</td> </tr> <cfoutput query="companies"> <tr>   <td>#CompanyName#&nbsp;</td>   <td>#ZipCode#&nbsp;</td>   <td nowrap>     [<a href="CompanyViewForm.cfm?key=#CompanyID#">View</a>]     [<a href="CompanyEditForm.cfm?key=#CompanyID#">Edit</a>]     [<a href="CompanyDeleteForm.cfm?key=#CompanyID#">Delete</a>]   </td> </tr> </cfoutput> </table> <p><a href="index.cfm">Chapter 18 Home Page</a></p> 

Now let's build the first part of the custom tag that generates a form field. Type the following code into a file named DisplayField.cfm:

 <!--- Ensure the tag's required attributes are passed in ---> <cfparam name="Attributes.fieldName" type="string"> <!--- Ensure the tag's optional attributes are defaulted if not passed in ---> <cfparam name="Attributes.fieldLabel" default="#Attributes.fieldName#" type="string"> <cfparam name="Attributes.numericField" type="boolean" default="No"> <cfif ThisTag.ExecutionMode EQ "Start">   <!--- Make the parent tag's attributes available to this child tag --->   <cfset parentData = GetBaseTagData("CF_DISPLAYFORM")>   <!--- Associate this child tag with its parent tag --->   <cfassociate basetag="CF_DISPLAYFORM">   <cfoutput>     <tr>     <td>       #Attributes.fieldLabel#     </td>     <td>       <input type="text" name="#Attributes.fieldName#">     </td>     </tr>   </cfoutput> </cfif> 

To create the field for an Add form, all we need is the name of the field (which matches the name of its corresponding database column), whether or not the column is a numeric data type (so we know how to formulate the SQL statement that will be sent to the database), and what to label the field. We're going to use bidirectional communication between the child and parent tags, so we'll set up that mechanism as well, from the beginning.

Now let's build the first part of the Add form tag. Type the following code into a file named DisplayForm.cfm:

 <!--- Ensure the tag's required attributes are passed in ---> <cfparam name="Attributes.tableName" type="string"> <!--- Ensure the tag's optional attributes are defaulted if not passed in ---> <cfparam name="Attributes.tableLabel" type="string"   default="#Attributes.tableName#"> <cfoutput> <cfif ThisTag.ExecutionMode EQ "Start">  <!--- Opening tag executed --->   <!--- Open the Form --->   <table cellspacing="2" cellpadding="2" border="1">   <form method="post" name="#Attributes.tableName#AddForm">   <tr>   <td colspan="2"><b>#Attributes.tableLabel# Add Form</b></td>   </tr> <cfelse>  <!--- Closing tag executed --->   <!--- Focus the first field --->   <script language="JavaScript" type="text/javascript">   for(var i = 0; i < document.forms[0].elements.length; i++)   {     if(document.forms[0].elements[i].type != "hidden")     {       document.forms[0].elements[i].focus();       break;     }   }   </script>   <!--- Close the Form --->   <tr>   <td>&nbsp;</td>   <td>   <input type="submit" name="Submit"   value="Add #Attributes.tableLabel#">   </td>   </tr>   </form>   </table>   <p><a href="#Attributes.tableName#List.cfm">#Attributes.tableLabel# List</a>   </p> </cfif> <!--- End test for ExecutionMode ---> </cfoutput> 

The Start mode outputs the opening <form> tag and supporting HTML markup, and the End mode outputs the script that focuses the cursor in the first visible form field, the Submit button, and the closing <form> tag and supporting HTML markup. There's also a hyperlink back to the list page.

Create a page named CompanyAddForm.cfm using the cf_DisplayForm and cf_DisplayField custom tags, so you can try out what you've created so far. Let's just add fields for a couple of the columns in the Company table for now:

 <cf_DisplayForm formType="Add" tableName="Company" keyColumn="CompanyID"   numericKey="Yes">   <cf_DisplayField fieldName="CompanyName" numericField="No">   <cf_DisplayField fieldName="ZipCode" numericField="No" fieldLabel="Zip Code"> </cf_DisplayForm> 

Run this page and you'll see a form that doesn't do anything other than display in the browser. We want to add the ability to insert the form data into the database. This is where we separate the form logic into an initial page-display phase and a PostBack phase. This is easy to do by simply placing a hidden field named PostBack into the form. When the page is initially displayed, there is nothing in the Form scope; but when the form is submitted, the Form scope will be populated with its fields and their values. So we simply test if PostBack is defined:

[View full width]

<!--- Ensure the tag's required attributes are passed in ---> <cfparam name="Attributes.tableName" type="string"> <!--- Ensure the tag's optional attributes are defaulted if not passed in ---> <cfparam name="Attributes.tableLabel" type="string" default="#Attributes.tableName#"> <cfoutput> <cfif NOT IsDefined("PostBack")> <!--- Not part of a PostBack ---> <cfif ThisTag.ExecutionMode EQ "Start"> <!--- Opening tag executed ---> <!--- Open the Form ---> <table cellspacing="2" cellpadding="2" border="1"> <form method="post" name="#Attributes.tableName#AddForm"> <input type="hidden" name="PostBack" value="1"> <tr> <td colspan="2"><b>#Attributes.tableLabel# Add Form</b></td> </tr> <cfelse> <!--- Closing tag executed, not as part of a PostBack ---> <!--- Focus the first field ---> <script language="JavaScript" type="text/javascript"> for(var i = 0; i < document.forms[0].elements.length; i++) { if(document.forms[0].elements[i].type != "hidden") { document.forms[0].elements[i].focus(); break; } } </script> <!--- Close the Form ---> <tr> <td>&nbsp;</td> <td> <input type="submit" name="Submit" value="Add #Attributes.tableLabel#"> </td> </tr> </form> </table> <p> <a href="#Attributes.tableName#List.cfm">#Attributes.tableLabel# List</a> </p> </cfif> <!--- End test for ExecutionMode ---> <cfelse> <!--- This page was requested as part of a PostBack ---> <cfif ThisTag.ExecutionMode EQ "Start"> <!--- Opening tag executed ---> <!--- An Add form doesn't need to do anything in Start mode ---> <cfelse> <!--- Closing tag executed ---> <!--- Assemble the lists of columns and their corresponding values ---> <cfset listOfColumns = ""> <cfset listOfValues = ""> <cfloop index="i" from="1" to="#ArrayLen(ThisTag.AssocAttribs)#"> <cfset listOfColumns = ListAppend(listOfColumns, ThisTag.AssocAttribs[i].fieldName)> <cfif ThisTag.AssocAttribs[i].numericField> <cfset listOfValues = ListAppend(listOfValues, Val(Form[ThisTag.AssocAttribs[i].fieldName]))> <cfelse> <cfset listOfValues = ListAppend(listOfValues, "'#Replace(Form[ThisTag.AssocAttribs[i].fieldName], "'", "''", "ALL")#'")> </cfif> </cfloop> <cfquery name="insertRecord" datasource="#Application.dbDSN#"> INSERT INTO #Attributes.tableName#( #listOfColumns# ) VALUES( #PreserveSingleQuotes(listOfValues)# ) </cfquery> <cflocation url="#Attributes.TableName#List.cfm?message=#Attributes.tableLabel# added to database." addToken="No"> </cfif> <!--- End test for End mode ---> </cfif> <!--- End test for PostBack ---> </cfoutput>

Look at the PostBack phase's End mode for a moment. Notice how we build the lists of columns and values from the child tag data passed into the parent tag's ThisTag.AssocAttribs array, and then execute the insert statement we build from these lists.

Update your DisplayForm.cfm to match the preceding listing, and then give it a try. Your form should look just like Figure 18.20, and submitting the form should insert the record into the database and display the list shown in Figure 18.21.

Figure 18.20. The Add form so far.


Figure 18.21. We now have a working Add form.


So far, so good, but Add forms are very simple. Edit forms are more complicated because a record must be retrieved from the database before the form is built and displayed. This is the point where we must introduce the formStatus property and further split execution into Preparing and Building passes, so we can retrieve the record to be edited in the Preparing pass and then build the form that will be populated with that record in the Building pass.

Adding Edit Form Functionality

The Edit form needs a couple more items, including a new attribute to specify the type of form to create (Add or Edit), and the column that contains a unique key for each record in the table containing the data (this example only works with single-column keys).

We also create an internal attribute named formStatus that tells both the form tag and its nested child tags which pass through the form is currently being processed. For now, we'll concentrate on just creating the Edit form:

[View full width]

<!--- Ensure the tag's required attributes are passed in ---> <cfparam name="Attributes.formType" type="string"> <cfparam name="Attributes.tableName" type="string"> <cfparam name="Attributes.keyColumn" type="string"> <!--- Ensure the tag's optional attributes are defaulted if not passed in ---> <cfparam name="Attributes.numericKey" type="boolean" default="No"> <cfparam name="Attributes.tableLabel" type="string" default="#Attributes.tableName#"> <cfoutput> <cfif NOT IsDefined("PostBack")> <!--- Not part of a PostBack ---> <cfif ThisTag.ExecutionMode EQ "Start"> <!--- Opening tag executed ---> <!--- Tell the child tags that we're preparing the form ---> <cfset Attributes.formStatus = "Preparing"> <!--- Open the Form ---> <table cellspacing="2" cellpadding="2" border="1"> <form method="post" name="#Attributes.tableName##Attributes.formType#Form"> <input type="hidden" name="PostBack" value="1"> <tr> <td colspan="2"><b>#Attributes.tableLabel# #Attributes.formType# Form</b> </td> </tr> <!--- The Edit form requires a key to be passed ---> <cfif ListFindNoCase("Edit", Attributes.formType)> <cfif NOT IsDefined("URL.key")> <cflocation url="#Attributes.TableName#List.cfm" addToken="No"> </cfif> <input type="hidden" name="Key" value="#URL.key#"> <cfelseif NOT ListFindNoCase("Add,Edit", Attributes.formType)> <cfthrow message="The form was called using an illegal formType value." type="CustomForm" errorcode="60010"> </cfif> <cfelse> <!--- Closing tag executed, not as part of a PostBack ---> <cfif Attributes.formStatus EQ "Preparing"> <cfif ListFindNoCase("Edit", Attributes.formType)> <!--- Assemble the list of columns to retrieve ---> <cfset listOfColumns = ""> <cfloop index="i" from="1" to="#ArrayLen(ThisTag.AssocAttribs)#"> <cfset listOfColumns = ListAppend(listOfColumns, ThisTag.AssocAttribs[i].fieldName)> </cfloop> <!--- Retrieve the record to display or edit ---> <cfquery name="retrieveRecord" datasource="#Application.dbDSN#"> SELECT #listOfColumns# FROM #Attributes.tableName# WHERE <cfif Attributes.numericKey> #Attributes.keyColumn# = #Val(URL.key)# <cfelse> #Attributes.keyColumn# = '#URL.key#' </cfif> </cfquery> </cfif> <!--- Tell the child tags that we're ready to build the form ---> <cfset Attributes.formStatus = "Building"> <!--- Pass execution to the first child tag ---> <cfexit method="LOOP"> </cfif> <cfif Attributes.formStatus EQ "Building"> <cfif ListFindNoCase("Add,Edit", Attributes.formType)> <!--- Focus the first field ---> <script language="JavaScript" type="text/javascript"> for(var i = 0; i < document.forms[0].elements.length; i++) { if(document.forms[0].elements[i].type != "hidden") { document.forms[0].elements[i].focus(); break; } } </script> </cfif> <!--- Close the Form ---> <tr> <td>&nbsp;</td> <td> <input type="submit" name="Submit" value="#Attributes.formType# #Attributes.tableLabel#"> </td> </tr> </form> </table> <p> <a href="#Attributes.tableName#List.cfm">#Attributes.tableLabel# List</a> </p> </cfif> </cfif> <!--- End test for ExecutionMode ---> <cfelse> <!--- This page was requested as part of a PostBack ---> <cfif ThisTag.ExecutionMode EQ "Start"> <!--- Opening tag executed ---> <!--- An Add form doesn't need to do anything in Start mode ---> <cfelse> <!--- Closing tag executed ---> <!--- Assemble the lists of columns and their corresponding values ---> <cfset listOfColumns = ""> <cfset listOfValues = ""> <cfloop index="i" from="1" to="#ArrayLen(ThisTag.AssocAttribs)#"> <cfset listOfColumns = ListAppend(listOfColumns, ThisTag.AssocAttribs[i].fieldName)> <cfif ThisTag.AssocAttribs[i].numericField> <cfset listOfValues = ListAppend(listOfValues, Val(Form[ThisTag.AssocAttribs[i].fieldName]))> <cfelse> <cfset listOfValues = ListAppend(listOfValues, "'#Replace(Form[ThisTag.AssocAttribs[i].fieldName], "'", "''", "ALL")#'")> </cfif> </cfloop> <cfquery name="insertRecord" datasource="#Application.dbDSN#"> INSERT INTO #Attributes.tableName#( #listOfColumns# ) VALUES( #PreserveSingleQuotes(listOfValues)# ) </cfquery> <cflocation url="#Attributes.TableName#List.cfm?message=#Attributes.tableLabel# added to database." addToken="No"> </cfif> <!--- End test for End mode ---> </cfif> <!--- End test for PostBack ---> </cfoutput>

The formStatus helps us solve a tough chicken-and-egg problem: The parent tag can't retrieve a record to populate the form fields until it knows what form fields it needs to populate, but it can't know what form fields to populate until the form fields execute.

So we set Attributes.formStatus to Preparing in the parent tag's Start mode. Then, when the parent tag is in Inactive mode while the child tags process, the child tag requests the parent data using GetBaseTagData() so it knows the parent tag is Preparing, and the child tags oblige by sending their own data up to their parent tag and not outputting any fields. Now the parent is in End mode, and we can retrieve the record.

In sum, we retrieve the record while Preparing in the End mode of the Initial Page Display. Make sense? Give it a try. Create a page named CompanyEditForm.cfm containing the following code, and access it by clicking an Edit link on CompanyList.cfm:

 <cf_DisplayForm formType="Edit" tableName="Company" keyColumn="CompanyID"   numericKey="Yes">   <cf_DisplayField fieldName="CompanyName" numericField="No">   <cf_DisplayField fieldName="ZipCode" numericField="No" fieldLabel="Zip Code"> </cf_DisplayForm> 

Notice that the only real difference between this Edit form and the preceding Add form is the value of the formType attribute.

Now let's add the ability of the Edit form to update the database in the PostBack pass (look at the switch-case statement I added toward the end of the following listing):

[View full width]

<!--- Ensure the tag's required attributes are passed in ---> <cfparam name="Attributes.formType" type="string"> <cfparam name="Attributes.tableName" type="string"> <cfparam name="Attributes.keyColumn" type="string"> <!--- Ensure the tag's optional attributes are defaulted if not passed in ---> <cfparam name="Attributes.numericKey" type="boolean" default="No"> <cfparam name="Attributes.tableLabel" type="string" default="#Attributes.tableName#"> <cfoutput> <cfif NOT IsDefined("PostBack")> <!--- Not part of a PostBack ---> <cfif ThisTag.ExecutionMode EQ "Start"> <!--- Opening tag executed ---> <!--- Tell the child tags that we're preparing the form ---> <cfset Attributes.formStatus = "Preparing"> <!--- Open the Form ---> <table cellspacing="2" cellpadding="2" border="1"> <form method="post" name="#Attributes.tableName##Attributes.formType#Form"> <input type="hidden" name="PostBack" value="1"> <tr> <td colspan="2"><b>#Attributes.tableLabel# #Attributes.formType# Form</b> </td> </tr> <!--- Edit, View, and Delete forms require a key to be passed ---> <cfif ListFindNoCase("Edit", Attributes.formType)> <cfif NOT IsDefined("URL.key")> <cflocation url="#Attributes.TableName#List.cfm" addToken="No"> </cfif> <input type="hidden" name="Key" value="#URL.key#"> <cfelseif NOT ListFindNoCase("Add,Edit", Attributes.formType)> <cfthrow message="The form was called using an illegal formType value." type="CustomForm" errorcode="60010"> </cfif> <cfelse> <!--- Closing tag executed, not as part of a PostBack ---> <cfif Attributes.formStatus EQ "Preparing"> <cfif ListFindNoCase("Edit", Attributes.formType)> <!--- Assemble the list of columns to retrieve ---> <cfset listOfColumns = ""> <cfloop index="i" from="1" to="#ArrayLen(ThisTag.AssocAttribs)#"> <cfset listOfColumns = ListAppend(listOfColumns, ThisTag.AssocAttribs[i].fieldName)> </cfloop> <!--- Retrieve the record to display or edit ---> <cfquery name="retrieveRecord" datasource="#Application.dbDSN#"> SELECT #listOfColumns# FROM #Attributes.tableName# WHERE <cfif Attributes.numericKey> #Attributes.keyColumn# = #Val(URL.key)# <cfelse> #Attributes.keyColumn# = '#URL.key#' </cfif> </cfquery> </cfif> <!--- Tell the child tags that we're ready to build the form ---> <cfset Attributes.formStatus = "Building"> <!--- Pass execution to the first child tag ---> <cfexit method="LOOP"> </cfif> <cfif Attributes.formStatus EQ "Building"> <cfif ListFindNoCase("Add,Edit", Attributes.formType)> <!--- Focus the first field ---> <script language="JavaScript" type="text/javascript"> for(var i = 0; i < document.forms[0].elements.length; i++) { if(document.forms[0].elements[i].type != "hidden") { document.forms[0].elements[i].focus(); break; } } </script> </cfif> <!--- Close the Form ---> <tr> <td>&nbsp;</td> <td> <input type="submit" name="Submit" value="#Attributes.formType# #Attributes.tableLabel#"> </td> </tr> </form> </table> <p> <a href="#Attributes.tableName#List.cfm">#Attributes.tableLabel# List</a> </p> </cfif> </cfif> <!--- End test for ExecutionMode ---> <cfelse> <!--- This page was requested as part of a PostBack ---> <cfif ThisTag.ExecutionMode EQ "Start"> <!--- Opening tag executed ---> <!--- Tell the child tags that we're preparing the form ---> <cfset Attributes.formStatus = "Preparing"> <cfelse> <!--- Closing tag executed ---> <cfswitch expression="#Attributes.formType#"> <cfcase value="Add"> <!--- Assemble the lists of columns and their corresponding values ---> <cfset listOfColumns = ""> <cfset listOfValues = ""> <cfloop index="i" from="1" to="#ArrayLen(ThisTag.AssocAttribs)#"> <cfset listOfColumns = ListAppend(listOfColumns, ThisTag.AssocAttribs[i].fieldName)> <cfif ThisTag.AssocAttribs[i].numericField> <cfset listOfValues = ListAppend(listOfValues, Val(Form[ThisTag.AssocAttribs[i].fieldName]))> <cfelse> <cfset listOfValues = ListAppend(listOfValues, "'#Replace(Form[ThisTag.AssocAttribs[i].fieldName], "'", "''", "ALL")#'")> </cfif> </cfloop> <cfquery name="insertRecord" datasource="#Application.dbDSN#"> INSERT INTO #Attributes.tableName#( #listOfColumns# ) VALUES( #PreserveSingleQuotes(listOfValues)# ) </cfquery> <cflocation url="#Attributes.TableName#List.cfm?message=#Attributes.tableLabel# added to database." addToken="No"> </cfcase> <cfcase value="Edit"> <!--- Assemble the lists of columns and their corresponding values ---> <cfset listOfSetStatements = ""> <cfloop index="i" from="1" to="#ArrayLen(ThisTag.AssocAttribs)#"> <cfif ThisTag.AssocAttribs[i].numericField> <cfset listOfSetStatements = ListAppend(listOfSetStatements, ThisTag.AssocAttribs[i].fieldName & " = " & Form[ThisTag.AssocAttribs[i].fieldName])> <cfelse> <cfset listOfSetStatements = ListAppend(listOfSetStatements, ThisTag.AssocAttribs[i].fieldName & " = " & "'#Replace(Form[ThisTag.AssocAttribs[i].fieldName], "'", "''", "ALL")#'")> </cfif> </cfloop> <cfquery name="updateRecord" datasource="#Application.dbDSN#"> UPDATE #Attributes.tableName# SET #PreserveSingleQuotes(listOfSetStatements)# <cfif Attributes.numericKey> WHERE #Attributes.keyColumn# = #Form.Key# <cfelse> WHERE #Attributes.keyColumn# = '#Form.Key#' </cfif> </cfquery> <cflocation url="#Attributes.TableName#List.cfm?message=#Attributes.tableLabel# updated in database." addToken="No"> </cfcase> </cfswitch> </cfif> <!--- End test for End mode ---> </cfif> <!--- End test for PostBack ---> </cfoutput>

That may seem like a lot of code, but it really isn't when you think about everything this code does. Let's concentrate for now on the child tags' Preparing pass through the form.

We start by opening the Edit form tag and redirecting requests that don't pass the required key parameter in the URL. If the URL contains the required key, control is passed to the child tags while the formStatus is set to Preparing. As you follow the explanation of this part of the code, refer to Figure 18.18:

 <!--- Ensure the tag's required attributes are passed in ---> <cfparam name="Attributes.fieldName" type="string"> <!--- Ensure the tag's optional attributes are defaulted if not passed in ---> <cfparam name="Attributes.fieldLabel" default="#Attributes.fieldName#"   type="string"> <cfparam name="Attributes.numericField" type="boolean" default="No"> <cfif ThisTag.ExecutionMode EQ "Start">   <!--- Make the parent tag's attributes available to this child tag --->   <cfset parentData = GetBaseTagData("CF_DISPLAYFORM")>   <cfif parentData.Attributes.formStatus EQ "Preparing">     <!--- Associate this child tag with its parent tag --->     <cfassociate basetag="CF_DISPLAYFORM">   </cfif>   <cfif parentData.Attributes.formStatus EQ "Building">     <cfoutput>       <tr>       <td>         #Attributes.fieldLabel#       </td>       <td>         <input type="text" name="#Attributes.fieldName#"           <cfif parentData.Attributes.formType EQ "Edit">             value="#parentData.retrieveRecord[Attributes.fieldName][1]#"           </cfif>>       </td>       </tr>     </cfoutput>   </cfif> </cfif> 

We added a test for Attributes.formStatus EQ "Building" so that the child tag outputs form fields only when the parent tag is in the Building pass, and we added a test for Attributes.formType to output an input value attribute only when we're building an Edit form.

Try it out, and you should have a form that looks like Figure 18.22, which updates the database as expected and as shown in Figure 18.23.

Figure 18.22. cf_DisplayForm and cf_DisplayField can now adapt to being either an Add or an Edit form.


Figure 18.23. The form can now also adapt to inserting for an Add form or updating for an Edit form.


Believe it or not, we now have the majority of what we need to create any kind of form. Watch how easy it is to add Delete form and View form functionality.

Adding Delete Form Functionality

Look through the following code for the word "Delete" and you'll see how easy it was to add Delete form functionality to what we've already built:

[View full width]

<!--- Ensure the tag's required attributes are passed in ---> <cfparam name="Attributes.formType" type="string"> <cfparam name="Attributes.tableName" type="string"> <cfparam name="Attributes.keyColumn" type="string"> <!--- Ensure the tag's optional attributes are defaulted if not passed in ---> <cfparam name="Attributes.numericKey" type="boolean" default="No"> <cfparam name="Attributes.tableLabel" type="string" default="#Attributes.tableName#"> <cfoutput> <cfif NOT IsDefined("PostBack")> <!--- Not part of a PostBack ---> <cfif ThisTag.ExecutionMode EQ "Start"> <!--- Opening tag executed ---> <!--- Tell the child tags that we're preparing the form ---> <cfset Attributes.formStatus = "Preparing"> <!--- Open the Form ---> <table cellspacing="2" cellpadding="2" border="1"> <form method="post" name="#Attributes.tableName##Attributes.formType#Form"> <input type="hidden" name="PostBack" value="1"> <tr> <td colspan="2"><b>#Attributes.tableLabel# #Attributes.formType# Form</b> </td> </tr> <!--- Edit and Delete forms require a key to be passed ---> <cfif ListFindNoCase("Edit,Delete", Attributes.formType)> <cfif NOT IsDefined("URL.key")> <cflocation url="#Attributes.TableName#List.cfm" addToken="No"> </cfif> <input type="hidden" name="Key" value="#URL.key#"> <cfelseif NOT ListFindNoCase("Add,Edit,Delete", Attributes.formType)> <cfthrow message="The form was called using an illegal formType value." type="CustomForm" errorcode="60010"> </cfif> <cfelse> <!--- Closing tag executed, not as part of a PostBack ---> <cfif Attributes.formStatus EQ "Preparing"> <cfif ListFindNoCase("Edit,Delete", Attributes.formType)> <!--- Assemble the list of columns to retrieve ---> <cfset listOfColumns = ""> <cfloop index="i" from="1" to="#ArrayLen(ThisTag.AssocAttribs)#"> <cfset listOfColumns = ListAppend(listOfColumns, ThisTag.AssocAttribs[i].fieldName)> </cfloop> <!--- Retrieve the record to display or edit ---> <cfquery name="retrieveRecord" datasource="#Application.dbDSN#"> SELECT #listOfColumns# FROM #Attributes.tableName# WHERE <cfif Attributes.numericKey> #Attributes.keyColumn# = #Val(URL.key)# <cfelse> #Attributes.keyColumn# = '#URL.key#' </cfif> </cfquery> </cfif> <!--- Tell the child tags that we're ready to build the form ---> <cfset Attributes.formStatus = "Building"> <!--- Pass execution to the first child tag ---> <cfexit method="LOOP"> </cfif> <cfif Attributes.formStatus EQ "Building"> <cfif ListFindNoCase("Add,Edit", Attributes.formType)> <!--- Focus the first field ---> <script language="JavaScript" type="text/javascript"> for(var i = 0; i < document.forms[0].elements.length; i++) { if(document.forms[0].elements[i].type != "hidden") { document.forms[0].elements[i].focus(); break; } } </script> </cfif> <!--- Close the Form ---> <tr> <td>&nbsp;</td> <td> <input type="submit" name="Submit" value="#Attributes.formType# #Attributes.tableLabel#"> </td> </tr> </form> </table> <p> <a href="#Attributes.tableName#List.cfm">#Attributes.tableLabel# List</a> </p> </cfif> </cfif> <!--- End test for ExecutionMode ---> <cfelse> <!--- This page was requested as part of a PostBack ---> <cfif ThisTag.ExecutionMode EQ "Start"> <!--- Opening tag executed ---> <!--- Tell the child tags that we're preparing the form ---> <cfset Attributes.formStatus = "Preparing"> <cfelse> <!--- Closing tag executed ---> <cfswitch expression="#Attributes.formType#"> <cfcase value="Add"> <!--- Assemble the lists of columns and their corresponding values ---> <cfset listOfColumns = ""> <cfset listOfValues = ""> <cfloop index="i" from="1" to="#ArrayLen(ThisTag.AssocAttribs)#"> <cfset listOfColumns = ListAppend(listOfColumns, ThisTag.AssocAttribs[i].fieldName)> <cfif ThisTag.AssocAttribs[i].numericField> <cfset listOfValues = ListAppend(listOfValues, Val(Form[ThisTag.AssocAttribs[i].fieldName]))> <cfelse> <cfset listOfValues = ListAppend(listOfValues, "'#Replace(Form[ThisTag.AssocAttribs[i].fieldName], "'", "''", "ALL")#'")> </cfif> </cfloop> <cfquery name="insertRecord" datasource="#Application.dbDSN#"> INSERT INTO #Attributes.tableName#( #listOfColumns# ) VALUES( #PreserveSingleQuotes(listOfValues)# ) </cfquery> <cflocation url="#Attributes.TableName#List.cfm?message=#Attributes.tableLabel# added to database." addToken="No"> </cfcase> <cfcase value="Edit"> <!--- Assemble the lists of columns and their corresponding values ---> <cfset listOfSetStatements = ""> <cfloop index="i" from="1" to="#ArrayLen(ThisTag.AssocAttribs)#"> <cfif ThisTag.AssocAttribs[i].numericField> <cfset listOfSetStatements = ListAppend(listOfSetStatements, ThisTag.AssocAttribs[i].fieldName & " = " & Form[ThisTag.AssocAttribs[i].fieldName])> <cfelse> <cfset listOfSetStatements = ListAppend(listOfSetStatements, ThisTag.AssocAttribs[i].fieldName & " = " & "'#Replace(Form[ThisTag.AssocAttribs[i].fieldName], "'", "''", "ALL")#'")> </cfif> </cfloop> <cfquery name="updateRecord" datasource="#Application.dbDSN#"> UPDATE #Attributes.tableName# SET #PreserveSingleQuotes(listOfSetStatements)# <cfif Attributes.numericKey> WHERE #Attributes.keyColumn# = #Form.Key# <cfelse> WHERE #Attributes.keyColumn# = '#Form.Key#' </cfif> </cfquery> <cflocation url="#Attributes.TableName#List.cfm?message=#Attributes.tableLabel# updated in database." addToken="No"> </cfcase> <cfcase value="Delete"> <cfquery name="deleteRecord" datasource="#Application.dbDSN#"> DELETE FROM #Attributes.tableName# <cfif Attributes.numericKey> WHERE #Attributes.keyColumn# = #Form.Key# <cfelse> WHERE #Attributes.keyColumn# = '#Form.Key#' </cfif> </cfquery> <cflocation url="#Attributes.TableName#List.cfm?message=#Attributes.tableLabel# deleted from database." addToken="No"> </cfcase> </cfswitch> </cfif> <!--- End test for End mode ---> </cfif> <!--- End test for PostBack ---> </cfoutput>

In DisplayField.cfm we now have to choose between outputting a form field for Add and Edit forms or a static value for Delete forms.

 <!--- Ensure the tag's required attributes are passed in ---> <cfparam name="Attributes.fieldName" type="string"> <!--- Ensure the tag's optional attributes are defaulted if not passed in ---> <cfparam name="Attributes.fieldLabel" default="#Attributes.fieldName#"   type="string"> <cfparam name="Attributes.numericField" type="boolean" default="No"> <cfif ThisTag.ExecutionMode EQ "Start">   <!--- Make the parent tag's attributes available to this child tag --->   <cfset parentData = GetBaseTagData("CF_DISPLAYFORM")>   <cfif parentData.Attributes.formStatus EQ "Preparing">     <!--- Associate this child tag with its parent tag --->     <cfassociate basetag="CF_DISPLAYFORM">   </cfif>   <cfif parentData.Attributes.formStatus EQ "Building">     <cfoutput>     <!--- Add and Edit forms output form fields --->     <cfif ListFindNoCase("Add,Edit", parentData.Attributes.formType)>       <tr>       <td>         #Attributes.fieldLabel#       </td>       <td>         <input type="text" name="#Attributes.fieldName#"           <cfif parentData.Attributes.formType EQ "Edit">             value="#parentData.retrieveRecord[Attributes.fieldName][1]#"           </cfif>>       </td>       </tr>     </cfif>     <!--- View and Delete forms output static values --->     <cfif ListFindNoCase("Delete", parentData.Attributes.formType)>       <tr>       <td>         #Attributes.fieldLabel#       </td>       <td>         #parentData.retrieveRecord[Attributes.fieldName][1]#       </td>       </tr>     </cfif>     </cfoutput>   </cfif> </cfif> 

Give it a try. Copy the contents of CompanyEditForm.cfm to a file named CompanyDeleteForm.cfm and change the value of the formType attribute to Delete. Then access this new file via a Delete link on CompanyList.cfm and try to delete a record or two.

So now you're getting the hang of it, eh? Let's wrap things up by adding View form functionality to what we've already done.

Adding View Form Functionality

Technically, there's no such thing as a "View form" because it's only a display page for a record that is never submitted anywhere, but it's still a useful feature for some systems that don't want to display Edit forms for data that doesn't need to be changed.

Look for the word "View" in Listings 18.14 and 18.15 to see what we've changed. These are the finished versions of the cf_DisplayForm and cf_DisplayField custom tags.

Listing 18.14. DisplayForm.cfmCompleted Form Custom Tag

[View full width]

 <!--- Author: Adam Phillip Churvis -- ProductivityEnhancement.com ---> <!--- Displays either an add, edit, view, or delete form ---> <!--- Ensure the tag's required attributes are passed in ---> <cfparam name="Attributes.formType" type="string"> <cfparam name="Attributes.tableName" type="string"> <cfparam name="Attributes.keyColumn" type="string"> <!--- Ensure the tag's optional attributes are defaulted if not passed in ---> <cfparam name="Attributes.numericKey" type="boolean" default="No"> <cfparam name="Attributes.tableLabel" type="string"   default="#Attributes.tableName#"> <cfoutput> <cfif NOT IsDefined("PostBack")>  <!--- Not part of a PostBack --->   <cfif ThisTag.ExecutionMode EQ "Start">  <!--- Opening tag executed --->     <!--- Tell the child tags that we're preparing the form --->     <cfset Attributes.formStatus = "Preparing">     <!--- Open the Form --->     <table cellspacing="2" cellpadding="2" border="1">     <form method="post" name="#Attributes.tableName##Attributes.formType#Form">     <input type="hidden" name="PostBack" value="1">     <tr>     <td colspan="2"><b>#Attributes.tableLabel# #Attributes.formType# Form</b>     </td>     </tr>     <!--- Edit, View, and Delete forms require a key to be passed --->     <cfif ListFindNoCase("Edit,View,Delete", Attributes.formType)>       <cfif NOT IsDefined("URL.key")>         <cflocation url="#Attributes.TableName#List.cfm" addToken="No">       </cfif>       <input type="hidden" name="Key" value="#URL.key#">     <cfelseif NOT ListFindNoCase("Add,Edit,View,Delete", Attributes.formType)>       <cfthrow message="The form was called using an illegal formType value."         type="CustomForm"         errorcode="60010">     </cfif>   <cfelse>  <!--- Closing tag executed, not as part of a PostBack --->     <cfif Attributes.formStatus EQ "Preparing">       <cfif ListFindNoCase("Edit,View,Delete", Attributes.formType)>         <!--- Assemble the list of columns to retrieve --->         <cfset listOfColumns = "">         <cfloop index="i" from="1" to="#ArrayLen(ThisTag.AssocAttribs)#">           <cfset listOfColumns = ListAppend(listOfColumns,             ThisTag.AssocAttribs[i].fieldName)>         </cfloop>         <!--- Retrieve the record to display or edit --->         <cfquery name="retrieveRecord" datasource="#Application.dbDSN#">         SELECT           #listOfColumns#         FROM           #Attributes.tableName#         WHERE         <cfif Attributes.numericKey>           #Attributes.keyColumn# = #Val(URL.key)#         <cfelse>           #Attributes.keyColumn# = '#URL.key#'         </cfif>         </cfquery>       </cfif>       <!--- Tell the child tags that we're ready to build the form --->       <cfset Attributes.formStatus = "Building">       <!--- Pass execution to the first child tag --->       <cfexit method="LOOP">     </cfif>     <cfif Attributes.formStatus EQ "Building">       <cfif ListFindNoCase("Add,Edit", Attributes.formType)>         <!--- Focus the first field --->         <script language="JavaScript" type="text/javascript">         for(var i = 0; i < document.forms[0].elements.length; i++)         {           if(document.forms[0].elements[i].type != "hidden")           {             document.forms[0].elements[i].focus();             break;           }         }         </script>       </cfif>       <!--- Close the Form --->       <cfif ListFindNoCase("Add,Edit,Delete", Attributes.formType)>         <tr>         <td>&nbsp;</td>         <td>         <input type="submit" name="Submit"         value="#Attributes.formType# #Attributes.tableLabel#">         </td>         </tr>       </cfif>       </form>       </table>       <p>       <a href="#Attributes.tableName#List.cfm">#Attributes.tableLabel# List</a>       </p>     </cfif>   </cfif> <!--- End test for ExecutionMode ---> <cfelse>  <!--- This page was requested as part of a PostBack --->   <cfif ThisTag.ExecutionMode EQ "Start">    <!--- Opening tag executed --->     <!--- Tell the child tags that we're preparing the form --->     <cfset Attributes.formStatus = "Preparing">   <cfelse>  <!--- Closing tag executed --->     <cfswitch expression="#Attributes.formType#">       <cfcase value="Add">         <!--- Assemble the lists of columns and their corresponding values --->         <cfset listOfColumns = "">         <cfset listOfValues = "">         <cfloop index="i" from="1" to="#ArrayLen(ThisTag.AssocAttribs)#">           <cfset listOfColumns = ListAppend(listOfColumns,             ThisTag.AssocAttribs[i].fieldName)>           <cfif ThisTag.AssocAttribs[i].numericField>             <cfset listOfValues = ListAppend(listOfValues,               Val(Form[ThisTag.AssocAttribs[i].fieldName]))>           <cfelse>             <cfset listOfValues = ListAppend(listOfValues,               "'#Replace(Form[ThisTag.AssocAttribs[i].fieldName], "'", "''",               "ALL")#'")>           </cfif>         </cfloop>         <cfquery name="insertRecord" datasource="#Application.dbDSN#">         INSERT INTO #Attributes.tableName#(           #listOfColumns#           )         VALUES(           #PreserveSingleQuotes(listOfValues)#           )         </cfquery>         <cflocation url="#Attributes.TableName#List.cfm?message=#Attributes.tableLabel#  added to database." addToken="No">       </cfcase>       <cfcase value="Edit">         <!--- Assemble the lists of columns and their corresponding values --->         <cfset listOfSetStatements = "">         <cfloop index="i" from="1" to="#ArrayLen(ThisTag.AssocAttribs)#">           <cfif ThisTag.AssocAttribs[i].numericField>             <cfset listOfSetStatements = ListAppend(listOfSetStatements,               ThisTag.AssocAttribs[i].fieldName & " = " &               Form[ThisTag.AssocAttribs[i].fieldName])>           <cfelse>             <cfset listOfSetStatements = ListAppend(listOfSetStatements,               ThisTag.AssocAttribs[i].fieldName & " = " &               "'#Replace(Form[ThisTag.AssocAttribs[i].fieldName], "'", "''",               "ALL")#'")>           </cfif>         </cfloop>         <cfquery name="updateRecord" datasource="#Application.dbDSN#">         UPDATE #Attributes.tableName#         SET #PreserveSingleQuotes(listOfSetStatements)#         <cfif Attributes.numericKey>           WHERE #Attributes.keyColumn# = #Form.Key#         <cfelse>           WHERE #Attributes.keyColumn# = '#Form.Key#'         </cfif>         </cfquery>         <cflocation url="#Attributes.TableName#List.cfm?message=#Attributes.tableLabel#  updated in database." addToken="No">       </cfcase>       <cfcase value="Delete">         <cfquery name="deleteRecord" datasource="#Application.dbDSN#">         DELETE FROM #Attributes.tableName#         <cfif Attributes.numericKey>           WHERE #Attributes.keyColumn# = #Form.Key#         <cfelse>           WHERE #Attributes.keyColumn# = '#Form.Key#'         </cfif>         </cfquery>         <cflocation url="#Attributes.TableName#List.cfm?message=#Attributes.tableLabel#  deleted from database." addToken="No">       </cfcase>     </cfswitch>   </cfif>  <!--- End test for End mode ---> </cfif> <!--- End test for PostBack ---> </cfoutput> 

Listing 18.15. DisplayField.cfmCompleted Field Custom Tag
 <!--- Author: Adam Phillip Churvis -- ProductivityEnhancement.com ---> <!--- Outputs either a text input or a static value depending upon the type of       form that contains it ---> <!--- Ensure the tag's required attributes are passed in ---> <cfparam name="Attributes.fieldName" type="string"> <!--- Ensure the tag's optional attributes are defaulted if not passed in ---> <cfparam name="Attributes.fieldLabel" default="#Attributes.fieldName#"   type="string"> <cfparam name="Attributes.numericField" type="boolean" default="No"> <cfif ThisTag.ExecutionMode EQ "Start">   <!--- Make the parent tag's attributes available to this child tag --->   <cfset parentData = GetBaseTagData("CF_DISPLAYFORM")>   <cfif parentData.Attributes.formStatus EQ "Preparing">     <!--- Associate this child tag with its parent tag --->     <cfassociate basetag="CF_DISPLAYFORM">   </cfif>   <cfif parentData.Attributes.formStatus EQ "Building">     <cfoutput>     <!--- Add and Edit forms output form fields --->     <cfif ListFindNoCase("Add,Edit", parentData.Attributes.formType)>       <tr>       <td>         #Attributes.fieldLabel#       </td>       <td>         <input type="text" name="#Attributes.fieldName#"           <cfif parentData.Attributes.formType EQ "Edit">             value="#parentData.retrieveRecord[Attributes.fieldName][1]#"           </cfif>>       </td>       </tr>     </cfif>     <!--- View and Delete forms output static values --->     <cfif ListFindNoCase("View,Delete", parentData.Attributes.formType)>       <tr>       <td>         #Attributes.fieldLabel#       </td>       <td>         #parentData.retrieveRecord[Attributes.fieldName][1]#       </td>       </tr>     </cfif>     </cfoutput>   </cfif> </cfif> 

How did this exercise feel for you? Advanced custom tags is not an easy subject to master, so don't feel bad if this chapter was a challenge. This is about as advanced as custom tag architecture gets, but you'll be glad you invested the time to build and understand it.



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