Multi-Step Form Demo In ColdFusion

Posted June 16, 2008 at 9:14 AM

Tags: ColdFusion

I was working on some client work the other day and dealing with a multi-step form process when it occurred to me that I never blogged about this type of thing. And so, I thought I would throw together a quick little multi-step form demo using ColdFusion. Because there are good number of files involved, I thought I would make a little demo video, before we get into the code, so you can see how it all comes together. NOTE: Sorry for the humming in the background - that's the air conditioner:


 
 
 

 
 
 
 
 

As you have seen, the demo form is really simple - two screens, each with a single form field, followed by a review page and then a confirmation page. Let's dive into the code, starting with the Application.cfc. I am not gonna spend too much time explaining how it all works since I think the code is fairly straight forward (and I have to get back to work ;)):

 Launch code in new window » Download code as text file »

  • <cfcomponent
  • output="false"
  • hint="I configure the application.">
  •  
  • <!--- Define application settings. --->
  • <cfset THIS.Name = "MultiPartFormDemo" />
  • <cfset THIS.ApplicationTimeout = CreateTimeSpan( 0, 0, 5, 0 ) />
  • <cfset THIS.SessionManagement = true />
  • <cfset THIS.SessionTimeout = CreateTimeSpan( 0, 0, 5, 0 ) />
  •  
  • <!--- Define page settings. --->
  • <cfsetting showdebugoutput="false" />
  •  
  •  
  • <cffunction
  • name="OnSessionStart"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I fire when a session starts.">
  •  
  • <!---
  • Create a struct to hold the multi-paart form data.
  • Each set of form data will be stored in a unique ID.
  • --->
  • <cfset SESSION.FormData = {} />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  • </cfcomponent>

This is just a demo, so I am not setting up anything in the Application except for the SESSION scope. And, since our form data needs to persist across multiple page requests, we need a way to cached it. We certainly don't want to submit all of our form data via hidden inputs (as that becomes unruly very quickly!), so we need to store it in a persistent scope. And, since we don't want this data to stick around for too long, let's store it in the user's SESSION scope so that when it timesout, we get our RAM back.

Since our demo is just for this form process, we are going to keep the form processing in our index.cfm, which you can see, does almost nothing:

 Launch code in new window » Download code as text file »

  • <!---
  • In our index file, we are going to include an action
  • page and a display page for the master form. Each of
  • these files will take care of the sub-steps of our
  • multi-step form.
  • --->
  • <cfinclude template="_form_act.cfm" />
  • <cfinclude template="_form_dsp.cfm" />

All this does is include the processing and display files for our form. Each of these files, in turn, handles the sub-steps for the form processing and display (respectively).

Next, let's look at the primary form processing page included above, _form_act.cfm. This is, by far, the most complicated page in the entire process:

 Launch code in new window » Download code as text file »

  • <!---
  • Create an attributes scope to combine the form and url
  • data into a single scope.
  • --->
  • <cfset REQUEST.Attributes = Duplicate( URL ) />
  • <cfset StructAppend( REQUEST.Attributes, FORM ) />
  •  
  • <!---
  • Param the form-ID. This is the unique identifier to hold
  • the data for this multi-step form.
  • --->
  • <cfparam
  • name="REQUEST.Attributes.form_id"
  • type="string"
  • default=""
  • />
  •  
  • <!--- Param the current step of the form process. --->
  • <cfparam
  • name="REQUEST.Attributes.step"
  • type="numeric"
  • default="1"
  • />
  •  
  • <!--- Param the form submission flag. --->
  • <cfparam
  • name="REQUEST.Attributes.submitted"
  • type="boolean"
  • default="false"
  • />
  •  
  •  
  • <!---
  • Check to see if we our current form-ID exists in the session
  • cache. If it does not, then we are hitting this multi-part
  • form for the first time.
  • --->
  • <cfif NOT StructKeyExists( SESSION.FormData, REQUEST.Attributes.form_id )>
  •  
  • <!---
  • Initializing the form data. Here, we don't need to
  • intialize the entire form data, just the missions
  • critical parts.
  • --->
  •  
  • <!--- Create a new ID. --->
  • <cfset REQUEST.Attributes.form_id = CreateUUID() />
  •  
  • <!---
  • Create a new struct to hold the form data. In this
  • struct, each step is going to be set to False; this
  • boolean will flag whether or not the given step has
  • been completed by the user.
  • --->
  • <cfset REQUEST.FormData = {
  • ID = REQUEST.Attributes.form_id,
  • Step1 = false,
  • Step2 = false,
  • Step3 = false,
  • Step = 1,
  • StepCount = 3
  • } />
  •  
  • <!---
  • Store the form data in the session cache using our
  • new UUID.
  • --->
  • <cfset SESSION.FormData[ REQUEST.FormData.ID ] = REQUEST.FormData />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • ASSERT: At this point, whether this is a first run page or
  • a sub-step, our multi-part form data struct has been created
  • and cached in our SESSION data cache. It also contains all
  • the mission critical data for the process.
  • --->
  •  
  •  
  • <!--- Get the form data out of the SESSION cache. --->
  • <cfset REQUEST.FormData = SESSION.FormData[ REQUEST.Attributes.form_id ] />
  •  
  •  
  • <!---
  • Check to see if our current step is a valid step. It is
  • valid if it is an available step number AND that the
  • previous step was completed. If the previous step was NOT
  • completed, then the user is trying to skip ahead.
  • --->
  • <cfif (
  • (NOT ListFind( "1,2,3", REQUEST.Attributes.step )) OR
  • (
  • (REQUEST.Attributes.step GT 1) AND
  • (NOT REQUEST.FormData[ "Step#(REQUEST.Attributes.step - 1)#" ])
  • ))>
  •  
  • <!---
  • The user has tried to access a step in the form that
  • does not exist or was not available (due to previous
  • step completion). Send them back to first step.
  • --->
  • <cfset REQUEST.FormData.Step = 1 />
  •  
  • <cfelse>
  •  
  • <!--- The step was valid, store it in the form data. --->
  • <cfset REQUEST.FormData.Step = REQUEST.Attributes.step />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Now that we have the step propertly evaluated, let's use the
  • step to update the rest of the form data. Everytime a user
  • goes to a given step, we want to make sure they have to work
  • their way BACK through the form. Therefore, step all forward-
  • facing steps (this one inclusive) to false.
  • --->
  • <cfloop
  • index="intStep"
  • from="#REQUEST.FormData.Step#"
  • to="#REQUEST.FormData.StepCount#"
  • step="1">
  •  
  • <!--- Set given step to false (not completed). --->
  • <cfset REQUEST.FormData[ "Step#intStep#" ] = false />
  •  
  • </cfloop>
  •  
  •  
  •  
  • <!--- Create an array to hold form data. --->
  • <cfset REQUEST.Errors = [] />
  •  
  • <!---
  • We have properly configured our FormData values. Include
  • the appropriate action file.
  • --->
  • <cfinclude template="_form_step#REQUEST.FormData.Step#_act.cfm" />

As you can see, there's a good amount of logic on this page, so let's take a moment to explore it. First, we check to see if the given form ID exists in our SESSION cache. If it does not, then we need to create a new form data cache and store it based on a new UUID (the easiest way to eliminate data collisions). At this point, the FormData object holds the mission critical pieces of data; namely, which step we are on, how many steps there are, and which steps have been completed (the boolean flags). Don't worry about the rest of the form data - we will handle that on each subsequent page.

After we have the form data initialized, we check to see if the currently requested step is valid. It is valid if it is both in the list of available steps AND that the previous step (if there was one) was completed. This prevents the user from jumping past a given step without finishing it. If a user does try to skip a step, we catch that and force them back to step 1 (you fought the law and the law won!).

Once the step validation is completed, we simply include the action file for the current step; each step must have its own action file and display file.

Now, let's take a quick look at the primary form display file, _form_dsp.cfm:

 Launch code in new window » Download code as text file »

  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>Multi-Step Form Demo</title>
  • </head>
  • <body>
  •  
  • <cfoutput>
  •  
  • <h1>
  • Multi-Step Form Demo
  • </h1>
  •  
  • <!---
  • Output a link for each form step so users can jump
  • around (if they want to).
  • --->
  • <ol>
  • <cfloop
  • index="intStep"
  • from="1"
  • to="#REQUEST.FormData.StepCount#"
  • step="1">
  •  
  • <li>
  • <a href="./index.cfm?form_id=#REQUEST.FormData.ID#&step=#intStep#">Step #intStep#</a>
  •  
  • <cfif (REQUEST.FormData.Step EQ intStep)>
  • <strong>&laquo;</strong>
  • </cfif>
  • </li>
  •  
  • </cfloop>
  • </ol>
  •  
  •  
  • <form
  • action="#CGI.script_name#"
  • method="post">
  •  
  • <!---
  • Submit form definition data back with the form.
  • This will allow each form step to know where it
  • left off. This also means that each step MUST
  • submit the form data back to itself - you cannot
  • simply move a person to another step.
  • --->
  • <input
  • type="hidden"
  • name="form_id"
  • value="#REQUEST.FormData.ID#"
  • />
  •  
  • <input
  • type="hidden"
  • name="step"
  • value="#REQUEST.FormData.Step#"
  • />
  •  
  • <!--- Flag the form as being submitted. --->
  • <input
  • type="hidden"
  • name="submitted"
  • value="true"
  • />
  •  
  •  
  • <!--- Check to see if there were any errors. --->
  • <cfif ArrayLen( REQUEST.Errors )>
  •  
  • <p>
  • Please review the following:
  • </p>
  •  
  • <ul>
  • <cfloop
  • index="strError"
  • array="#REQUEST.Errors#">
  •  
  • <li>
  • #strError#
  • </li>
  •  
  • </cfloop>
  • </ul>
  •  
  • </cfif>
  •  
  •  
  • <!--- Include the form step. --->
  • <cfinclude
  • template="_form_step#REQUEST.FormData.Step#_dsp.cfm"
  • />
  •  
  • </form>
  •  
  • </cfoutput>
  •  
  • </body>
  • </html>

This page simply sets up the page and form wrapper that goes around each of the multi-part form steps. Nothing complicated going on here at all.

That's it for the framework of the multi-part form process. Now, we can quickly go through each step. The one thing to take note of is that each action file params the cached FormData values. This way, the framework itself does not have to care about what data we are collection - it only has to step up the mechanism for processing steps. After each FormData value is paramed, the FORM field data is paramed using the cached data; this allows us to repopulate the form if the user comes back to the given step for a second time.

Step 1 Action File

 Launch code in new window » Download code as text file »

  • <!---
  • Param cached form data. We only need to cached the
  • form data that will be made available for this step.
  • --->
  • <cfparam
  • name="REQUEST.FormData.Name"
  • type="string"
  • default=""
  • />
  •  
  •  
  • <!---
  • Param form / attribute data. As we param the form data, use
  • the values in the cached data (in case we are hitting this
  • step for a second time.
  • --->
  • <cfparam
  • name="FORM.name"
  • type="string"
  • default="#REQUEST.FormData.Name#"
  • />
  •  
  •  
  • <!--- Check to see if form was submitted. --->
  • <cfif REQUEST.Attributes.submitted>
  •  
  • <!--- Validate form data. --->
  •  
  • <cfif NOT Len( FORM.name )>
  •  
  • <cfset ArrayAppend(
  • REQUEST.Errors,
  • "Please enter your name"
  • ) />
  •  
  • </cfif>
  •  
  •  
  • <!--- Check to see if we have any errors. --->
  • <cfif NOT ArrayLen( REQUEST.Errors )>
  •  
  • <!--- Store the form data in our cache. --->
  • <cfset REQUEST.FormData.Name = FORM.name />
  •  
  • <!--- Flag this step as being completed. --->
  • <cfset REQUEST.FormData.Step1 = true />
  •  
  • <!--- Forward user to next step. --->
  • <cflocation
  • url="./index.cfm?form_id=#REQUEST.FormData.ID#&step=2"
  • addtoken="false"
  • />
  •  
  • </cfif>
  •  
  • </cfif>

Step 1 Display File

 Launch code in new window » Download code as text file »

  • <cfoutput>
  •  
  • <label>
  • Name:
  •  
  • <input
  • type="text"
  • name="name"
  • value="#FORM.name#"
  • maxlength="30"
  • size="40"
  • />
  • </label>
  • <br />
  • <br />
  •  
  • <input type="submit" value="Submit Form" />
  •  
  • </cfoutput>

Step 2 Action File

 Launch code in new window » Download code as text file »

  • <!---
  • Param cached form data. We only need to cached the
  • form data that will be made available for this step.
  • --->
  • <cfparam
  • name="REQUEST.FormData.Birthday"
  • type="string"
  • default=""
  • />
  •  
  •  
  • <!---
  • Param form / attribute data. As we param the form data, use
  • the values in the cached data (in case we are hitting this
  • step for a second time.
  • --->
  • <cfparam
  • name="FORM.birthday"
  • type="string"
  • default="#REQUEST.FormData.Birthday#"
  • />
  •  
  •  
  • <!--- Check to see if form was submitted. --->
  • <cfif REQUEST.Attributes.submitted>
  •  
  • <!--- Validate form data. --->
  •  
  • <cfif NOT IsDate( FORM.birthday )>
  •  
  • <cfset ArrayAppend(
  • REQUEST.Errors,
  • "Please enter your birthday"
  • ) />
  •  
  • </cfif>
  •  
  •  
  • <!--- Check to see if we have any errors. --->
  • <cfif NOT ArrayLen( REQUEST.Errors )>
  •  
  • <!--- Store the form data in our cache. --->
  • <cfset REQUEST.FormData.Birthday = FORM.birthday />
  •  
  • <!--- Flag this step as being completed. --->
  • <cfset REQUEST.FormData.Step2 = true />
  •  
  • <!--- Forward user to next step. --->
  • <cflocation
  • url="./index.cfm?form_id=#REQUEST.FormData.ID#&step=3"
  • addtoken="false"
  • />
  •  
  • </cfif>
  •  
  • </cfif>

Step 2 Display File

 Launch code in new window » Download code as text file »

  • <cfoutput>
  •  
  • <label>
  • Birthday:
  •  
  • <input
  • type="text"
  • name="birthday"
  • value="#FORM.birthday#"
  • maxlength="20"
  • size="20"
  • />
  • </label>
  • <br />
  • <br />
  •  
  • <input type="submit" value="Submit Form" />
  •  
  • </cfoutput>

Steps 1 and 2 were for collecting data; step 3, the last step in our process is for data review and form processing. In this step, the user has the chance to view all of their entered data and to skip back to a previous step if they need to change anything.

Step 3 Action File

 Launch code in new window » Download code as text file »

  • <!--- Check to see if form was submitted. --->
  • <cfif REQUEST.Attributes.submitted>
  •  
  • <!---
  • The user has confirmed that the data they have submitted
  • is correct. Now, do something with the data (ie. insert
  • into database) and send the user to a confirmation page.
  •  
  • Also, you can delete the FORM data from the SESSION if you
  • want to free up some memory.
  •  
  • ex.
  • StructDelete( SESSION, REQUEST.FormData.ID )
  • --->
  •  
  • <cflocation
  • url="./confirm.cfm"
  • addtoken="false"
  • />
  •  
  • </cfif>

Step 3 Display File

 Launch code in new window » Download code as text file »

  • <cfoutput>
  •  
  • <p>
  • Thanks for taking the time to fill out this form.
  • Please review the data below:
  • </p>
  •  
  • <ul>
  • <li>
  • <strong>Name:</strong>
  • #REQUEST.FormData.Name#
  • </li>
  • <li>
  • <strong>Birthday:</strong>
  • #REQUEST.FormData.Birthday#
  • </li>
  • </ul>
  •  
  •  
  • <input type="submit" value="Submit Data" />
  •  
  • </cfoutput>

That's all there is to it. Sorry for running through those last few files, but as you can see, once you have the framework of the multi-part form processing setup, the subsequent step files (both action and display) are quite simple and act more or less like stand-alone pages. The process is easy to scale since you only have to create more action and display files.

Download Code Snippet ZIP File

Comments (12)  |  Post Comment  |  Ask Ben  |  Permalink  |  Other Searches  |  Print Page



I'm Too Young For This!

Reader Comments

Awesome!

I currently have two projects on hold because I wasn't sure which was the best approach for a multi-step form processing. Your solution is very similar to one of my options but waayyy better handled by you.

Thank you so much for posting this.

Thanks to you I will go back to my projects and finished them very soon.

Posted by Rosana Levesque on Jun 16, 2008 at 1:32 PM


@Rosana,

Thank you very much for the kind words. I am glad that this will help you. I have found this method to be very helpful. I think it is very important that each step of the process submit *back to itself*. Even if the only action in a step is click on a link.

Meaning, image you are in Step 2 and let's stay you had to select an account ID, do NOT do this:

[a href="./?form_id=#...#&step=3&account_id=8"]Account [/a]

This would move the person to the next step without processing it. Instead, submit back to step 2:

[a href="./?form_id=#...#&step=2&account_id=8&submitted=true"]Account A [/a]

This submits back to Step 2 with the given account ID. The "submitted" flag will get us to process the step, store the Account ID, flag the step as being completed, and then push the user to the next step.

This method keeps YOU in control of the work flow.

Good luck!

Posted by Ben Nadel on Jun 16, 2008 at 1:40 PM


Hi Ben,

Wondering why you use Duplicate() for the URL vars:

<cfset REQUEST.Attributes = Duplicate( URL ) />

Won't the URL struct be a single layer only, so no need to get a "deep" copy?

Nice tutorial.

Posted by Michael Sharman on Jun 17, 2008 at 7:59 AM


@Michael,

I have run into weird problems one or two times using StructCopy() instead of Duplicate(). I could never debug what the problem was, but it always went away if I switched to using Duplicate(). So, I just started using it as a matter of practice.

Posted by Ben Nadel on Jun 17, 2008 at 8:33 AM


Great article Ben, thanks for sharing it :)

As far as for the difference between structCopy() and duplicate() is that structCopy() deep-copies only the first level values of the structure. Duplicate() deep-copies all the nested levels.

Check the following code, notice how both sData.sInnerData.cVal and sStrCopyData.sInnerData.sVal changes but the sDupData didn't.

<cfset sData = structNew() />

<cfset sData.cVal = "Test" />
<cfset sData.sInnerData = structNew() />
<cfset sData.sInnerData.cVal = "Test" />

<cfset sStrCopyData = structCopy(sData) />
<cfset sDupData = duplicate(sData) />

<cfset sData.cVal = "Test1" />
<cfset sData.sInnerData.cVal = "Test1" />

<cfdump var="#sData#" label="sData" />
<cfdump var="#sStrCopyData#" label="sStrCopyData" />
<cfdump var="#sDupData#" label="sDupData" />

Posted by Angelos on Jun 17, 2008 at 1:05 PM


@Angelos,

Thanks for the demo code. I have had some really weird issues though, where it seems almost as if the top-level variables were copied by reference, not by value. I'm talking just strings too. Very odd. I'll see if I can reproduce the error at all.

Posted by Ben Nadel on Jun 17, 2008 at 1:13 PM


Ben,

I don't quite understand why you choose to use a formID (REQUEST.Attributes.form_id = CreateUUID()).

I read your comment: (..the easiest way to eliminate data collisions..), could you elaborate some on that?

Is it to ensure that two different computers would not be accessing the same session because someone how they have the same CFID / CFToken? Or is there more to it?

Thanks!

Randy

Posted by Randy Johnson on Jun 17, 2008 at 4:30 PM


@Randy,

It's to ensure that a given user does not have two different forms open with the same ID (think: two different windows / tabs on two separate multi-step form processes). I am not worried about different users as this data is user-specific. This is to stop a user from colliding with herself.

I could get a unique value a different way, but a UUID is just rather convenient for this.

Posted by Ben Nadel on Jun 17, 2008 at 4:33 PM


@Ben,

ok that makes sense. Here is an example to see if I understand: Let's say I took phone applications for a mortgage company .

Person 1 calls in I start the three page form. I have to stop on page 2 because they have to look in their file cabinet for something.

I switch lines and start talking with Person 2, I enter in their information and hit submit. Now they are page 2.

If I didn't have the unique form identifier in there then I would have just overwrote person 1's information.

Is that correct?

-Randy

Posted by Randy Johnson on Jun 17, 2008 at 4:48 PM


@Randy,

Exactly.

Posted by Ben Nadel on Jun 17, 2008 at 4:50 PM


In this example, there are several things that do not work in CF 7. Besides how the struct and array are defined, is there anything else that will not work in CF 7? I do not have the luxury yet to use CF 8.

Posted by CFConfused on Jun 24, 2008 at 4:14 PM


@CFConfused,

I think the only things ColdFusion 8 specific are the struct / array implicit creations and the array loop. Everything else is pretty basic ColdFusion.

Posted by Ben Nadel on Jun 25, 2008 at 7:37 AM


Post Comment  |  Ask Ben


Home   |   Web Log   |   ColdFusion   |   Projects   |   Resume   |   Job Form   |   Search   |   Contact
Epicenter Consulting - Custom Software Solutions for Business Evolution HostMySite.com - The Leader In ColdFusion Hosting