ColdFusion in Context: Better Buttons and Images

You may have noticed by now that there's a natural pattern in working with lists of records to copy, edit, or delete. You let the user click on a link (sometimes camoflaged by an image) to pass a key value in a URL to a page that lets you do these things.

However, there's another reason you usually use a link instead of a submit button: the value of a submit button is displayed on the face of the button to the user. Admit it; a button bearing a record ID can wind up looking like someone's home or your driver's license emblazoned on your T-shirt. You can do better.

I recently wound up in a situation where page "A", a control page, passes a bunch of form fields to page "B" to cause page "B" to display a list that's limited by these form field parameters. So far, so good.

However, toggling fields in a record and immediately redisplaying the newly changed list is a different proposition. People expect to see where they left off. Deletion is another example of an operation where people expect to come right back and see that the row is really missing from the sliding data window they just left. In either case, you need someplace to keep the query criteria and its starting place, and this is a big chunk of data.

Add and Edit operations are often coded without this consideration. Users spend enough time away from the list that it almost seems reasonable to start over when they get back. However, when there's no delay, you need a better alternative.

Alternatives?

Some may say, "Just use javascript." True, you can pass a value as a parameter of the onClick function you've addressed as an attribute of an input tag of type="button". This would let you pass the data in form fields. However, an input of type="button" is not a true form control by itself. It's useless to individuals whose talking browsers don't understand javascript.

Some may say to just use a big query string. However, this invites curious users to see what minor changes will do. Also, the amount of data you can pass this way isn't big and isn't consistent.

Part of the issue is that you have to know which row the user selected. Some may say to make each row a separate form. You can do that, but then you have to repeat all the query criteria in hidden fields again and again for EACH row.

Some may say to put the data into session variables or cookies. However, this can get very confusing if your business model expects a user to have multiple windows open at once. In the case of session variables, it also eats up memory on your server. In the case of cookies, it fails when you least expect it. Cookie storage is limited, and faulty proxy servers make write-once, read-many a more reliable proposition for cookies than write-once, read-once.

Use a Better Button

So what is one to do? Use the humble input type=submit, but read its name instead of its value. Or, use the input=type image, but read one of its names (ending in .X or .Y) instead of its value.

Consider a moment what you want to accomplish. You want to pass a bunch of data that pertains to the whole page, and you want to know which row was selected. To do this, simply give the button or image input a name that will submit the form and whose name will tell you which row was involved.

Provide Data

Create table Flipper with these columns and this data. It just gives us something to work with.

RowID,Code,StatusInd
5,RR,0
12,CC,0
22,SS,1
41,BB,1
83,AA,0
96,QQ,0

List

Put this code in buttons.cfm and let it include other code to perform special operations. For this example, we'll put a submit button on every row. The name of the button will be TFlip followed by the Row ID. By pressing a button, the user will be able to flip the status of the corresponding row.

First, if the form.Running is defined - it's a hidden field whose purpose is to tell you that the form has been submitted, look for a field name containing TFlip to see if one of these buttons has been pressed. Why name the buttons TFlip{row ID} instead of Flip{row ID}? To avoid confusion in case the word "flip" is embedded in another form field name someday. To look for TFlip, check the submitted form fields. The submitted form fields are in a structure named form. The structKeyList function returns a list consisting of the key names (field names of the submitted form in this case) in the structure. You can find the first item in the list that contains the partial text you're looking for by using the listContainsNoCase function. If the text "TFlip" is found, include flip.cfm to flip the status of the selected row. After flip.cfm is executed, control returns to this point.

<cfif isDefined("form.Running")>
  <cfset Hit=listContainsNoCase(structKeyList(form),"TFlip")>
  <cfif Hit>
    <cfinclude template="flip.cfm">
  </cfif>
</cfif>

Run a list query, and begin the form and header for the list.

<cfquery name="statusList" datasource="context">
select * from Flipper
order by Code
</cfquery>

<form method="post">
<table><tr>
<td>CODE</td>
<td>STATUS</td>
</tr>

Loop through rows and end the form. Define the symbol you want to use to represent the row's status in the database. Provide two sets of controls with one commented out. The first set merely uses a submit button whose name is TFlip{row ID}. (The value doesn't matter except for display.) The set that's commented out uses an input tag of type="image" for which you've specified the same name and which specifies a source of your own choosing for the image. (We'll try the image tag later.) After the loop, a hidden tag named Running in the form itself tells the next pass through the page that the form has been submitted.

<cfoutput>
<cfloop query="statusList">
<tr>
<td>#Code#</td>
<cfif StatusInd>
  <cfset StatusFlag="X">
<cfelse>
  <cfset StatusFlag="O">
</cfif>
<td>
<input type="submit" name="TFlip#RowID#" value="$">
<!--- Replace the previous line with this one later
<input type="image" name="TFlip#RowID#" src="paw.gif">
--->
#StatusFlag#
</td>
</tr>
</cfloop>
<input type="hidden" name="Running" value="Yes">
</cfoutput>
</table>
</form>

Get the Row ID

Put the remaining code in flip.cfm. If Running does not exist, tell the user and stop. If no field has TFlip in its name, tell the user and stop.

In comments, you'll see an action to remove the last two characters. That's for future use when you try this code again with images. If you create an input of type="image" and name it "Fred", the form structure will contain TWO fields, Fred.X and Fred.Y, each containing a coordinate corresponding to where the user clicked the image. In this case, you only care about a name, so cropping the last two characters makes sense.

The reason for cropping the first five characters is to remove "TFlip". What remains is supposed to be a Row ID. If the name exists but doesn't have a number greater than zero after it, tell the user and stop. Otherwise, what you have left is a candidate row ID.

<cfif not isDefined("Running")>
  The form was not submitted; press back button and retry.
  <cfabort>
</cfif>
<cfset Hit=listContainsNoCase(structKeyList(form),"TFlip")>
<cfif not Hit>
  Action to be taken was not provided; press back button and retry.
  <cfabort>
</cfif>
<cfset HitName=listGetAt(structKeyList(form),Hit)>
<!--- ADD next line to use image
  <cfset HitName=left(HitName,len(HitName)-2)>
--->
<cfset HitRow=right(HitName,len(HitName)-5)>
<cfif not val(HitRow)>
  Row identifier not provided; press back button and retry.
  <cfabort>
</cfif>

Find and Flip

Now you need to flip the current status of the row to its alternative. But first, could this query have been avoided? You could pass the Row ID in a hidden field with a special name, find the field the same way you found the button, and read its value. You could let the status be 1 or -1 and simply multiply the current status by -1 to flip it. This wouldn't let you use a bit field for storage and would force you to interpret it slightly differently. However, it's an idea worth considering; because, you wouldn't need to know the current status at all in order to change it.

For this demo, a simple query is used as shown here. If the row ID isn't found, tell the user and stop.

<cfquery name="statusGet" datasource="context">
select StatusInd
from Flipper
where RowID = #HitRow#
</cfquery>
<cfif not statusGet.recordcount>
  The row ID is not in the table; press back button and retry.
</cfif>

What's left is easy. Change the status from what it is to the alternative.

<cfquery name="statusSet" datasource="context">
update Flipper
<cfif statusGet.StatusInd>
  set StatusInd = 0
<cfelse>
  set StatusInd = 1
</cfif>
where RowID = #HitRow#
</cfquery>

Try it with Images and Text

Copy buttons.cfm to buttons2.cfm; copy flip.cfm to flip2.cfm, and make some changes in the copies. In buttons2.cfm, obey the comment to switch the type="submit" tag with the type="image" tag. Change the src (source) name in the example to the name of a small image that you have handy (preferably a gif for browser compatibility). Change the template name in the include statement from flip.cfm to flip2.cfm. In flip2.cfm, uncomment the line that drops the last two characters from the button name.

Browse buttons2.cfm. The status should show up as Xs and Os. Click an image to watch its status change. Do the same with buttons.cfm. Both methods work. Both are acceptable. You have just created better buttons and images.

Consider that your typical page has a control that says, in effect, leave this page entirely and return to the main menu. Maybe you used a link to do this in the past. Maybe you used javascript with input type="button". Now you have a better alternative. =Marty=