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 »
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 »
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 »
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 »
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.
Launch code in new window » Download code as text file »
Launch code in new window » Download code as text file »
Launch code in new window » Download code as text file »
Launch code in new window » Download code as text file »
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.
Launch code in new window » Download code as text file »
Launch code in new window » Download code as text file »
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
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