The "ColdFusion Sample" series is a collection of blog entries that describe how to do common tasks in web development with ColdFusion. They are not meant to be the only solution, but simply one solution, presented in such a fashion that you can take the code and immediately use it (with modifications of course) in a production environment. For today's entry I'm going to demonstrate how to handle a multi-step form in ColdFusion. My example will be the fairly common checkout form seen on e-commerce sites. Multi-step forms can be done completely client side. For this entry though I'm going to rely on a simpler solution. One quick aside. I try like heck to keep my code samples as simple as possible. So for example, if I wanted to demonstrate how to output the current time in ColdFusion, I'm not going to bother with a 'complete' HTML page. While all of the extra HTML may make for a "Proper" HTML result, it's stupid to clutter up one line of CFML with 10 lines of HTML. That being said I decided to try out the Bootstrap toolkit to make my demo look a bit nicer. At the very end of this blog entry I'll have a few notes on Bootstrap and what I both liked and disliked.

Before we get into the code, let's describe at a high level how we can handle a multi-step form. There a few different approaches:

  1. Each step of the form is one HTML page. State data (what you entered already) is preserved in hidden form fields.
  2. Each step of the form is one HTML page. State data (what you entered already) is stored in a persistent server side scope unique to the user.
  3. The entire form is loaded onto the client and CSS/JS is used to show/hide one step at a time.

In approach one, you have to ensure that you store the previous data in hidden form fields. This can be get cumbersome especially after a few steps. I already mentioned that we weren't going to cover the completely client-side approach. So for this demo, we will make use of ColdFusion's Session scope. Let's then begin with our Application.cfc file:

component { this.name="formdemo"; this.sessionManagement="true";

public void function onSessionEnd(sessionScope,appScope) { //consider logging an incomplete form }

}

Two things I want to point out here. First, notice I'm not using onSessionStart. You may be thinking, "Don't we want to default some data for the form?" We may. But - in a complete web site, you can't assume all users are going to actually perform a check out. Therefore we will only setup the data when we need to. Secondly - what about the empty onSessionEnd? This is purely a reminder about the fact that you can run things on a session expiring. I've blogged/preached about this feature before and I truly don't think it is used often enough. Now let's move on to the first step.

step1.cfm

<!--- Initial check to see if we have a core structure to store our data. ---> <cfif not structKeyExists(session, "checkout")> <cfset session.checkout = {}> </cfif> <!--- initial defaults for the first section ---> <cfif not structKeyExists(session.checkout, "bio")> <cfset session.checkout.bio = {firstname="", lastname="", email=""}> </cfif>

<!--- form fields will default according to session values ---> <cfparam name="form.firstname" default="#session.checkout.bio.firstname#"> <cfparam name="form.lastname" default="#session.checkout.bio.lastname#"> <cfparam name="form.email" default="#session.checkout.bio.email#">

<cfif structKeyExists(form, "submit")>

<cfset errors = []> <cfset form.firstname = trim(htmlEditFormat(form.firstname))> <cfset form.lastname = trim(htmlEditFormat(form.lastname))> <cfset form.email = trim(htmlEditFormat(form.email))> <cfif not len(form.firstname)> <cfset arrayAppend(errors, "First name is empty.")> </cfif> <cfif not len(form.lastname)> <cfset arrayAppend(errors, "Last name is empty.")> </cfif> <cfif not len(form.email) or not isValid("email", form.email)> <cfset arrayAppend(errors, "Email is empty or not correct.")> </cfif>

<cfif not arrayLen(errors)> <cfset session.checkout.bio = {firstname=form.firstname, lastname=form.lastname, email=form.email}> <cflocation url="step2.cfm" addToken="false"> </cfif>

</cfif>

<!DOCTYPE html> <html> <head> <title>Check Out Form - Step 1</title> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1" /> <link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.3.0/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script> <script src="http://twitter.github.com/bootstrap/1.3.0/bootstrap-alerts.js"></script> </head> <body>

<div class="container">

<h1>Step 1: Your Name</h1>

<cfif structKeyExists(variables, "errors")> <div class="alert-message block-message error" data-alert="alert"> <a class="close" href="#">×</a> <p><strong>Oh snap! You got an error!</strong></p> <ul> <cfloop index="error" array="#errors#"> <cfoutput> <li>#error#</li> </cfoutput> </cfloop> </ul> </div> </cfif>

<form method="post"> <cfoutput> <div class="clearfix"> <label for="firstname">Your Name:</label> <div class="input"> <input type="text" name="firstname" id="firstname" placeholder="First" value="#form.firstname#"> <input type="text" name="lastname" id="lastname" placeholder="Last" value="#form.lastname#"> </div> </div>

<div class="clearfix"> <label for="email">Your Email Address:</label> <div class="input"> <input type="email" name="email" id="email" value="#form.email#"> </div> </div> </cfoutput>

<div class="actions"> <input type="submit" class="btn primary" name="submit" value="Save"> </div>

</form>

</div>

</body> </html>

Ok, we've got a lot going on here so let's approach this at a high level (ie, not just top to bottom). I will, though, point out the top of the file handles some start up logic necessary for the process. I begin by initializing a checkout structure. This is going to hold all of the data for the entire process. Next, I default a structure just for this step. I've named it bio and it includes three keys.

The file is a self-posting form. What that means is that when you hit submit, it's going to send the FORM data to itself. This makes handling errors a lot simpler. In the bottom half of the page you can see our form consists of 3 fields: 2 for our name and one for an email address. If you go back up towards the top, you can see my form validation. This by itself shouldn't be new. But pay attention to what we do when no errors exist. We copy the value to the session scope, specifically session.checkout.bio, and then use cflocation to move to the next step. Now let's look at step 2.

step2.cfm

<!--- If no checkout, send them to step 1 ---> <cfif not structKeyExists(session, "checkout")> <cflocation url="step1.cfm" addToken="false"> </cfif> <!--- initial defaults for the section ---> <cfif not structKeyExists(session.checkout, "address")> <cfset session.checkout.address = {street="", city="", state="", postal=""}> </cfif>

<!--- form fields will default according to session values ---> <cfparam name="form.street" default="#session.checkout.address.street#"> <cfparam name="form.city" default="#session.checkout.address.city#"> <cfparam name="form.state" default="#session.checkout.address.state#"> <cfparam name="form.postal" default="#session.checkout.address.postal#">

<cfif structKeyExists(form, "submit")>

<cfset errors = []> <cfset form.street = trim(htmlEditFormat(form.street))> <cfset form.city = trim(htmlEditFormat(form.city))> <cfset form.state = trim(htmlEditFormat(form.state))> <cfset form.postal = trim(htmlEditFormat(form.postal))>

<cfif not len(form.street)> <cfset arrayAppend(errors, "Street is empty.")> </cfif> <cfif not len(form.city)> <cfset arrayAppend(errors, "City is empty.")> </cfif> <cfif not len(form.state)> <cfset arrayAppend(errors, "State is empty.")> </cfif> <cfif not len(form.postal) or not isValid("zipcode", form.postal)> <cfset arrayAppend(errors, "Postal code is empty or not correct.")> </cfif>

<cfif not arrayLen(errors)> <cfset session.checkout.address = {street=form.street, city=form.city, state=form.state, postal=form.postal}> <cflocation url="step3.cfm" addToken="false"> </cfif>

</cfif>

<!DOCTYPE html> <html> <head> <title>Check Out Form - Step 2</title> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1" /> <link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.3.0/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script> <script src="http://twitter.github.com/bootstrap/1.3.0/bootstrap-alerts.js"></script> </head> <body>

<div class="container">

<h1>Step 2: Your Address</h1>

<cfif structKeyExists(variables, "errors")> <div class="alert-message block-message error" data-alert="alert"> <a class="close" href="#">×</a> <p><strong>Oh snap! You got an error!</strong></p> <ul> <cfloop index="error" array="#errors#"> <cfoutput> <li>#error#</li> </cfoutput> </cfloop> </ul> </div> </cfif>

<form method="post"> <cfoutput> <div class="clearfix"> <label for="street">Your Street:</label> <div class="input"> <input type="text" name="street" id="street" value="#form.street#"> </div> </div>

<div class="clearfix"> <label for="city">Your City:</label> <div class="input"> <input type="text" name="city" id="city" value="#form.city#"> </div> </div>

<div class="clearfix"> <label for="state">Your State:</label> <div class="input"> <cfinclude template="states.cfm"> </div> </div>

<div class="clearfix"> <label for="street">Your Postal Code:</label> <div class="input"> <input type="text" name="postal" id="postal" value="#form.postal#"> </div> </div>

</cfoutput>

<div class="actions"> <input type="submit" class="btn primary" name="submit" value="Save"> </div>

</form>

</div>

</body> </html>

For the most part, this template follows the flow of the previous one, but notice that the very beginning of the file checks to see if the checkout structure exists. If it doesn't, then we want to push the user back to step 1. All of the next few steps will follow this exact same logic. Outside of that though nothing else is different. Step 2 deals with the address so I use the "location" of session.checkout.address. The fields here are different, but the flow is the same. Step 3 is the exact same but with shipping data instead.

step3.cfm

<!--- If no checkout, send them to step 1 ---> <cfif not structKeyExists(session, "checkout")> <cflocation url="step1.cfm" addToken="false"> </cfif> <!--- initial defaults for the section ---> <cfif not structKeyExists(session.checkout, "shippingaddress")> <cfset session.checkout.shippingaddress = {street="", city="", state="", postal=""}> </cfif>

<!--- form fields will default according to session values ---> <cfparam name="form.street" default="#session.checkout.shippingaddress.street#"> <cfparam name="form.city" default="#session.checkout.shippingaddress.city#"> <cfparam name="form.state" default="#session.checkout.shippingaddress.state#"> <cfparam name="form.postal" default="#session.checkout.shippingaddress.postal#">

<cfif structKeyExists(form, "submit")>

<cfset errors = []> <cfset form.street = trim(htmlEditFormat(form.street))> <cfset form.city = trim(htmlEditFormat(form.city))> <cfset form.state = trim(htmlEditFormat(form.state))> <cfset form.postal = trim(htmlEditFormat(form.postal))>

<cfif not len(form.street)> <cfset arrayAppend(errors, "Street is empty.")> </cfif> <cfif not len(form.city)> <cfset arrayAppend(errors, "City is empty.")> </cfif> <cfif not len(form.state)> <cfset arrayAppend(errors, "State is empty.")> </cfif> <cfif not len(form.postal) or not isValid("zipcode", form.postal)> <cfset arrayAppend(errors, "Postal code is empty or not correct.")> </cfif>

<cfif not arrayLen(errors)> <cfset session.checkout.shippingaddress = {street=form.street, city=form.city, state=form.state, postal=form.postal}> <cflocation url="step4.cfm" addToken="false"> </cfif>

</cfif>

<!DOCTYPE html> <html> <head> <title>Check Out Form - Step 3</title> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1" /> <link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.3.0/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script> <script src="http://twitter.github.com/bootstrap/1.3.0/bootstrap-alerts.js"></script> </head> <body>

<div class="container">

<h1>Step 3: Your Shipping Address</h1>

<cfif structKeyExists(variables, "errors")> <div class="alert-message block-message error" data-alert="alert"> <a class="close" href="#">×</a> <p><strong>Oh snap! You got an error!</strong></p> <ul> <cfloop index="error" array="#errors#"> <cfoutput> <li>#error#</li> </cfoutput> </cfloop> </ul> </div> </cfif>

<form method="post"> <cfoutput> <div class="clearfix"> <label for="street">Your Street:</label> <div class="input"> <input type="text" name="street" id="street" value="#form.street#"> </div> </div>

<div class="clearfix"> <label for="city">Your City:</label> <div class="input"> <input type="text" name="city" id="city" value="#form.city#"> </div> </div>

<div class="clearfix"> <label for="state">Your State:</label> <div class="input"> <cfinclude template="states.cfm"> </div> </div>

<div class="clearfix"> <label for="street">Your Postal Code:</label> <div class="input"> <input type="text" name="postal" id="postal" value="#form.postal#"> </div> </div>

</cfoutput>

<div class="actions"> <input type="submit" class="btn primary" name="submit" value="Save"> </div>

</form>

</div>

</body> </html>

And here is our 4th step, the one that handles credit card data.

step4.cfm

<!--- If no checkout, send them to step 1 ---> <cfif not structKeyExists(session, "checkout")> <cflocation url="step1.cfm" addToken="false"> </cfif> <!--- initial defaults for the section ---> <cfif not structKeyExists(session.checkout, "ccinfo")> <cfset session.checkout.ccinfo = {number="", name="", expmonth="", expyear=""}> </cfif>

<!--- form fields will default according to session values ---> <cfparam name="form.number" default="#session.checkout.ccinfo.number#"> <cfparam name="form.name" default="#session.checkout.ccinfo.name#"> <cfparam name="form.expmonth" default="#session.checkout.ccinfo.expmonth#"> <cfparam name="form.expyear" default="#session.checkout.ccinfo.expyear#">

<cfif structKeyExists(form, "submit")>

<cfset errors = []> <cfset form.number = trim(htmlEditFormat(form.number))> <cfset form.name = trim(htmlEditFormat(form.name))> <cfset form.expmonth = trim(htmlEditFormat(form.expmonth))> <cfset form.expyear = trim(htmlEditFormat(form.expyear))>

<cfif not len(form.number) or not isValid("creditcard" ,form.number)> <cfset arrayAppend(errors, "Credit card number is empty.")> </cfif> <cfif not len(form.name)> <cfset arrayAppend(errors, "Name on credit card is empty.")> </cfif> <cfif not len(form.expmonth)> <cfset arrayAppend(errors, "Expiration month not selected.")> </cfif> <cfif not len(form.expyear)> <cfset arrayAppend(errors, "Expiration year not selected.")> </cfif> <cfif len(form.expyear) and len(form.expmonth) and form.expyear is year(now()) and form.expmonth lt month(now())> <cfset arrayAppend(errors, "Credit card expiration is in the past.")> </cfif>

<cfif not arrayLen(errors)> <cfset session.checkout.ccinfo = {number=form.number, name=form.name, expmonth=form.expmonth, expyear=form.expyear}> <cflocation url="step5.cfm" addToken="false"> </cfif>

</cfif>

<!DOCTYPE html> <html> <head> <title>Check Out Form - Step 4</title> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1" /> <link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.3.0/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script> <script src="http://twitter.github.com/bootstrap/1.3.0/bootstrap-alerts.js"></script> </head> <body>

<div class="container">

<h1>Step 4: Your Credit Card</h1>

<cfif structKeyExists(variables, "errors")> <div class="alert-message block-message error" data-alert="alert"> <a class="close" href="#">×</a> <p><strong>Oh snap! You got an error!</strong></p> <ul> <cfloop index="error" array="#errors#"> <cfoutput> <li>#error#</li> </cfoutput> </cfloop> </ul> </div> </cfif>

<form method="post"> <cfoutput> <div class="clearfix"> <label for="number">Your Credit Card Number:</label> <div class="input"> <input type="text" name="number" id="number" value="#form.number#"> </div> </div>

<div class="clearfix"> <label for="name">Name on Credit Card:</label> <div class="input"> <input type="text" name="name" id="name" value="#form.name#"> </div> </div>

<div class="clearfix"> <label for="expmonth">CC Expiration:</label> <div class="input"> <select class="small" name="expmonth" id="expmonth"> <cfloop index="x" from="1" to="12"> <option value="#x#" <cfif form.expmonth is x>selected</cfif>>#monthAsString(x)#</option> </cfloop> </select> <select class="small" name="expyear" id="expyear"> <cfloop index="x" from="#year(now())#" to="#year(now())+10#"> <option value="#x#" <cfif form.expyear is x>selected</cfif>>#x#</option> </cfloop> </select> </div> </div>

</cfoutput>

<div class="actions"> <input type="submit" class="btn primary" name="submit" value="Save"> </div>

</form>

</div>

</body> </html>

Now let's look at step5 - the confirmation page. This one is pretty simple since there isn't really a form involved. I did keep a form in the page though so the user has to do a real form submission to move on. Notice I provide links back to the earlier steps.

step5.cfm

<!--- If no checkout, send them to step 1 ---> <cfif not structKeyExists(session, "checkout")> <cflocation url="step1.cfm" addToken="false"> </cfif>

<cfscript> /**

  • Escapes a credit card number, showing only the last 4 digits. The other digits are replaced with the * character.
  • return just stars if str too short, found by Tony Monast
  • @param ccnum Credit card number you want to escape. (Required)
  • @return Returns a string.
  • @author Joshua Miller (josh@joshuasmiller.com)
  • @version 2, April 26, 2009 / function ccEscape(ccnum){ if(len(ccnum) lte 4) return "**"; return "#RepeatString("",val(Len(ccnum)-4))##Right(ccnum,4)#"; } </cfscript>

<cfif structKeyExists(form, "submit")>

<!--- do something ---> <!--- clear info ---> <cfset session.checkout = {}> <cflocation url="step6.cfm" addToken="false">

</cfif>

<!DOCTYPE html> <html> <head> <title>Check Out Form - Step 5</title> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1" /> <link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.3.0/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script> <script src="http://twitter.github.com/bootstrap/1.3.0/bootstrap-alerts.js"></script> </head> <body>

<div class="container">

<h1>Step 5: Confirm</h1>

<cfif structKeyExists(variables, "errors")> <div class="alert-message block-message error" data-alert="alert"> <a class="close" href="#">×</a> <p><strong>Oh snap! You got an error!</strong></p> <ul> <cfloop index="error" array="#errors#"> <cfoutput> <li>#error#</li> </cfoutput> </cfloop> </ul> </div> </cfif>

<cfoutput> <h2>You</h2> <div class="row"> <div class="span2"> Name: </div> <div class="span6"> #session.checkout.bio.firstname# #session.checkout.bio.lastname# </div> </div> <div class="row"> <div class="span2"> Email: </div> <div class="span6"> #session.checkout.bio.email# </div> </div>

<p> <a href="step1.cfm">[Return to Step 1]</a> </p>

<h2>Your Address</h2> <address> #session.checkout.address.street#<br/> #session.checkout.address.city#, #session.checkout.address.state# #session.checkout.address.postal# </address>

<p> <a href="step2.cfm">[Return to Step 2]</a> </p>

<h2>Your Shipping Address</h2> <address> #session.checkout.shippingaddress.street#<br/> #session.checkout.shippingaddress.city#, #session.checkout.shippingaddress.state# #session.checkout.shippingaddress.postal# </address>

<p> <a href="step3.cfm">[Return to Step 3]</a> </p>

<h2>Your Credit Card</h2> <div class="row"> <div class="span2"> Number: </div> <div class="span6"> #ccEscape(session.checkout.ccinfo.number)# </div> </div> <div class="row"> <div class="span2"> Name on Card: </div> <div class="span6"> #session.checkout.ccinfo.name# </div> </div> <div class="row"> <div class="span2"> Expired: </div> <div class="span6"> #session.checkout.ccinfo.expmonth#/#session.checkout.ccinfo.expyear# </div> </div>

<p> <a href="step4.cfm">[Return to Step 4]</a> </p>

</cfoutput>

<form method="post">

<div class="actions"> <input type="submit" class="btn primary" name="submit" value="Checkout"> </div>

</form>

</div>

</body> </html>

All in all, not much to it, right? Most of the code so far really has been the HTML. The final step is really just a thank you page. I'll include it here for completeness sake.

step6.cfm

<!DOCTYPE html> <html> <head> <title>Check Out Form - Step 1</title> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1" /> <link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.3.0/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script> <script src="http://twitter.github.com/bootstrap/1.3.0/bootstrap-alerts.js"></script> </head> <body>

<div class="container">

<h1>Thank You</h1>

<p> Prosciutto drumstick tri-tip brisket flank meatball shank capicola. Turkey cow shankle pork chop tail, tenderloin strip steak pastrami ball tip meatball capicola ham. Pig meatloaf pork chop bresaola t-bone. Shankle pork chop chuck kielbasa ham hock, cow ball tip tongue prosciutto filet mignon beef biltong meatball. Ground round sirloin turkey turducken, chuck fatback flank jerky bresaola beef. Bresaola spare ribs pancetta, flank beef ribeye ground round pastrami chicken pork loin rump turducken. Salami turkey drumstick, shoulder pork chop shankle jerky prosciutto leberkäse beef t-bone brisket short loin. </p>

</div>

</body> </html>

You can try this code by hitting the big demo button below. For a credit card number, use 4111111111111111. I did not post the code for the states include but I've attached all the code as a zip to this blog entry.

Finally, some notes on the Bootstrap framework. I really like it a lot. It was pretty easy to make my pages look a hell of a lot better than they normally do. I do wish they had proper documentation. I know I can view source to see how they built stuff, but it would be nice to have it on screen too while looking at the demos. Also, some things are missing. So for example, they demonstrate a normal and medium sized select. I guessed that there was a small one and I was right, but why not - you know - document it so you don't have to guess?

Download attached file.