Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with:

Multi-Step Form Demo In ColdFusion

By Ben Nadel on
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 ;)):

  • <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:

  • <!---
  • 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:

  • <!---
  • 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:

  • <!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

  • <!---
  • 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

  • <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

  • <!---
  • 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

  • <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

  • <!--- 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

  • <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.

Tweet This Titillating read by @BenNadel - Multi-Step Form Demo In ColdFusion Thanks my man — you rock the party that rocks the body!


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.

Reply to this Comment

@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!

Reply to this Comment

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.

Reply to this Comment

@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.

Reply to this Comment

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" />

Reply to this Comment

@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.

Reply to this Comment

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

Reply to this Comment

@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.

Reply to this Comment

@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

Reply to this Comment

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.

Reply to this Comment

@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.

Reply to this Comment

I found your tutorial for multiple pages. why is it there aren't many tutorials on this given subject? I can't imagine this is such a slam dunk topic that it works for everybody all the time. i've always had problems with 2 areas in CF, multiple page forms and architecture frameworks. (tired of resetting form fields)

i tried tweaking your code to add one little feature (a back button) but I couldn't get it to work just right. there was always something problematic.

the more i kept making changes, the more confused I became. what changes would you make in order to incorporate a back button in the middle steps? in this case, i added a step 4 and step 5 so step 2-4 contain back buttons.

finally, just want to thank you for a plethora of great CF tutorials. you code is exceptionally clean, organized, and well laid out.

to be honest, i would rather read a cf book written by you rather than some of the regular authors(no names will be mentioned). your tutorials are very useful, practical, and I enjoy learning from you.

thanks

Reply to this Comment

@Mike,

First off, I'm really glad you are liking my demonstrations - that means a lot. That said, I don't think is a slam-dunk topic by any means. I think, simply, not very many people have multi-step forms.

Someone else, in fact, just asked me about this kind of stuff, specifically with the question of CFCs. I'll see if I can create a new, updated demo covering that and a back button.

Reply to this Comment

I'm a relative novice to advanced CF development and I understand the concepts of the tutorial but I'm having a hard time figuring out what to save the action and display pages as. Is the cflocation tag hard coded or is there something is the session scope I'm missing?

PLEASE HELP!!

Reply to this Comment

Hi Ben:
My name is Henry Cox. I am a CF developer who has been on the sidelines for a while but happy to be coming back up to speed. I like your multistep form here. I am curious to know what your approach to this would be with the new ORM and hibernate features of coldfusion 9 of CF9? I've seen Ben and Ray's videos on this and there seems to be a lot of excitement building, at least for me there is.
Thanks
Henry

Reply to this Comment

@Henry,

I don't think this would really change with ORM. The form data, to me, is outside of the ORM realm. You still need to pass form data from page to page in a multi-step form. You can maybe keep that in an ORM-based object, but I don't think that would always make sense.

Plus, because an ORM session only lasts for the page request, you might run into session issues as described by Mark Mandel over here:

http://www.compoundtheory.com/?action=displayPost&ID=419

As such, I think you'd still be best having simple form data being cached from page to page and *then* be added to the ORM-enabled objects only as a final persistence step.

Reply to this Comment

is there a way to use this method when one of your steps involves a file upload? I've been working with this but the biggest problem I've encountered is it doesn't remember the file and you get an error thrown when you go back to that step and then forward. My guess is i'd have to store the file in another hidden field or something to check against but what if the user wants to upload a new file that is named the same as the old?

am I making sense at all?

Reply to this Comment

@Tom,

I actually just worked with a similar situation. What I did was store the uploaded file path information in the form data struct (as part of the session values: REQUEST.FormData.UploadedFile). Then, when the form gets processed, you simply use that file path.

The thing that I have not come up with a good solution for is what to do with that file if the form never gets finished. I guess you could have a process that wipes it out every night, or perhaps gets triggers OnSessionEnd(). I'm not happy with the file stuff as far as clean up. But, as far as the multi-step form issue - just store it like you would store the other data.

Reply to this Comment

Hello;
I am trying to use your code. I am a little confused, Is this like 10 (or so) different pages running this? or for all the actions, do you put them all on the _form_act.cfm or do they each one have to have their own action file like action1.cfm and so on down the line?

Also the same thing with how you display the form, is that all part of the _form_dsp.cfm or do you have a file for each form per step. like step1.cfm and so on? if so, how do I change the include link code?

I'm new to this, sorry if this seems like an ignorant question. But it looks like this is the structure I need for a form like this.

Reply to this Comment

@Mjc,

Sorry for the confusion - in later posts I have been better about displaying the names of the files that I am using.

Essentially it breaks down like this (where each indent is an included file:

form page
. . . form ACTION page
. . . . . . Step 1 Action page
. . . . . . Step 2 Action page
. . . . . . Step N Action page
. . . form DISPLAY page
. . . . . . Step 1 Display page
. . . . . . Step 2 Display page
. . . . . . Step N Display page

Only one action and display page is included at any given time. So, if Step 1 Action page is included, Step 1 Display page would also be included.

I hope this helps.

Reply to this Comment

Hi Ben,

Thanks for a great demo, I was really struggling to find a good multi-step solution for form input & processing and this gives me the perfect framework to move forward.

The only problem that I'm having is that my development environment is CF8 and (unfortunately) my production environment is still MX7. I see from your comments ("I think the only things ColdFusion 8 specific are the struct / array implicit creations and the array loop."). I'm reasonably new to CF8 and haven't touched MX7! Any pointers to re-writing the code for the struct/array and array loop?

Yes! I know! Get a provider with CF8, it would be so much easier for sure, but, it's what I'm stuck with for the time being.

Cheers

Reply to this Comment

Nicely written tutorial Ben. Haven't touched CF in a long time and this really got me past all those session var gotchas. Thanks for the valuable contribution!

Reply to this Comment

@Giles,

Converting to CF7 would probably just be things like convert:

<cfset myArray = [] />
<cfset myStruct = {} />

... into this:

<cfset myArray = arrayNew() />
<cfset myStruct = structNew() />

@Jason,

Awesome my man - glad this is helping you ease back into ColdFusion - I hope you're happy being back.

Reply to this Comment

I had a question. I have the script to create the multi step form.. I have all the steps in 5 different pages... Do I put the entire scrip on one URL?..

Like in the example is all this going onto a single url page (signup.htm) or is it pulling from different pages (step1.htm, step2.htm) and going back to the main "signup.htm" page.. sorry if this sounds confusing but i'm lost.

Can somebody please help.

Reply to this Comment

I hope this isn't inappropriate to ask on this fourm but if i provided the script for my form and hosting password.. could I pay someone $50 in PayPal funds to put it together for me?

Reply to this Comment

@Carl,

I my example, they all go to the same script (top-level file), but the steps are controlled by URL parameters. The actual action files are then broken up into different physical files which are included into the top-level file.

Reply to this Comment

Hey Ben,

This looks like a great solution for me. I really appreciate you sharing it! Before I dive into this, can you tell me if this framework can handle a form consisting of 8 steps, with each step including anywhere from 5 to 30 fields?

Reply to this Comment

@Valerie,

Definitely; there is not inherent limitation here, other than what you can keep straight in your head :) Let us know how it goes.

Reply to this Comment

@Ben,

Could you provide an example of the specific ACTION and DISPLAY file naming convention for your multi-part form process? I understand all the process except that part isn't clear.

Also, suggestions for modifying the process to dynamically pull form information from DB tables? For example, a questionnaire with X number of questions presented in 10 record increments until X=recordcount, where the answers are also dynmically generated with cfselect (options 1-7) and the values from the question number and answer value are paired and captured on each form iteration? I'll happily pay you for the solution! :)

Great tutorial - I have spent many hours trying to determine the best way to solve the multi-form issue! Your solution is excellent!

Thanks,

Billy

Reply to this Comment

@Billy,

In this demo, I had one master "action" file and one master "display" file:

_form_act.cfm
_form_dsp.cfm

These files then include the step-based action and display files:

_form_step1_act.cfm
_form_step1_dsp.cfm

... where "step1" is the current step (vs. step2, step3, stepN, etc.).

As far as pulling from a database and making something very dynamic, that's really building a much bigger feature - a bit beyond what I think can reasonably be covered by a blog post.

Reply to this Comment

Great demo. Was heading towards this route, but I didn't know how to handle the possible collisions within a session. Thanks for the info!

Reply to this Comment

@Graham,

My pleasure. I've been using this approach for a while and have really liked it. One thing that I have found to be awesome is that if you ever need to save a form across sessions (like a "draft" or anything), you can easily convert the form data to and from WDDX and either store to XML files or to the database.

This WDDX conversion stuff can cause some issues with numbers, but typically, it's pretty nice.

Reply to this Comment

What am I doing wrong? I am getting the following error message when accessing the index.cfm:

Element FORMDATA is undefined in SESSION.

Reply to this Comment

This is the line that is causing the error:

41 : <cfif NOT StructKeyExists(SESSION.FormData, REQUEST.Attributes.form_id)>

Not sure why when I have the application.cfc in the directory which sets an empty form.Data struct.

Reply to this Comment

@Ben,

Please see my latest posts to this blog.

I really need to get this resolved.

Any suggestions?

Thank you

Reply to this Comment

@Monique,

Perhaps you are not building the request.attributes object correctly. It might not be the session.formData aspect of that code that is causing the problem.

Reply to this Comment

@Ben,

but the error specifically says Element FORMDATA is undefined in SESSION.

And I am using your exact code.

Reply to this Comment

@Monique,

Do you have session management enabled? Check your error logs to see if there is a hidden error you're not seeing.

Reply to this Comment

@Ben,

Yes, is there a demo of this code somewhere? The "video demo" on this page just shows a black screen for me (?)

Which file do I access first? Index.cfm?

I am lost :(

Reply to this Comment

@Monique,

Yeah, I accidentally deleted the video a long time ago :) The index.cfm file should be the only one you actually access (from what I remember). Then, the index file takes care of including the appropriate action and display files.

Reply to this Comment

@Monique,

Are you CFIncluding a file in your Application.cfc? It looks like the error originates in the Application.cfc file.

Reply to this Comment

@Ben,

Nope. My application.cfc is just like yours

<cfcomponent
output="false"
hint="I configure the application.">

<!--- Define application settings. --->
<cfset THIS.Name = "MultiPartFormDemo" />
<cfset THIS.ApplicationTimeout = CreateTimeSpan( 0, 5, 0, 0 ) />
<cfset THIS.SessionManagement = true />
<cfset THIS.SessionTimeout = CreateTimeSpan( 0, 5, 0, 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>

Reply to this Comment

@Monique,

I am stumped! Try CFDump'ing your Session right before the line that errors - see if you can spot something helpful. Otherwise, I have no idea why that's not working.

Reply to this Comment

@Monique,
Try placing two cfdumps on your index.cfm page with var="#FORM#" and the second var="#SESSION#".

Reply to this Comment

@Monique

Do you have any other coldfusion script running in that machine that uses Session Management successfully? From what I can see, the session is not starting, therefore the "OnSessionStart" function is not firing up. Just to make sure it is, I would change the output on that function to "true" and plug in some text inside that function just to see if it is executing the code inside or not.

I am using the exact same code in CF8 and it works just perfect

@Ben

Is it possible that her Coldfusion Server has sessions disabled? It sounds pretty unlikely but as I said, It appears the "OnSessionStart" function is not executing.

Reply to this Comment

@Rosana,

When I tried her pages, and cleared my cookies, it looksed like CFID, CFTOKEN, and JSESSIONID are all being created; so, some sort of session management is working. I don't know much about JSESSIONID as I only use basic session management.

This is quite the stumper!

Reply to this Comment

Hi Ben! Thank you very much for this article! I am a novice, and this is very easy to understand and works like a charm!

I do have one question though, and I hope you will be able to help me.

I have included several more fields on the form, and they are all working perfectly except the dropdown menus. (I am using CF8 BTW)

I have two dropdown boxes. One is for gender, so it is static (Male, Female) the other is for school attended, the school one is dynamic and pulls its list from the database.

It works for the other 8 fields on the page, just not my two drop down boxes. The rest of the fields are plain text boxes.

This is on the action page:

<cfset REQUEST.FormData.school = FORM.school />

This is the code for the school dropdown on the display page:

<select name="school" value="school">
<option> Select a School </option>
<cfoutput query="school_list">
<option value="#school_id#>#id_description#</option>
</cfoutput>
</select>

Any help would be greatly appreciated!

Reply to this Comment

Sorry, I should have clarified. The issue is that it wont cashe the data. The drop down boxes display perfectly on the page... it just wont cashe the data!

Reply to this Comment

Hi Ben (and community),
This snippet has been very helpful to me but I I'm curious about use of session variables.

If I'm understanding correctly you create a reference to request.formdata in the session during the initialization of the form.

That persists the data across the session even though you are never explicitly writing to the session variable.

1. Why doesn't writing to request.formdata require a session lock? (or does it but it was left out as not relevant to the demo)

2. Would you use this method for a form with 10 questions, or 1000 questions? How big can the formdata struct get before it's too big/no longer best practice to be persisted in RAM?

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.