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:
- Each step of the form is one HTML page. State data (what you entered already) is preserved in hidden form fields.
- 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.
- 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:
public void function onSessionEnd(sessionScope,appScope) {
//consider logging an incomplete form
} }
component {
this.name="formdemo";
this.sessionManagement="true";
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?
Archived Comments
Very cool. I am now more interested in exploring Bootstrap. Seems pretty cool for us non-designers and maybe easier to implement then jQuery UI.
I did notice an issue with the demo. I mistakenly entered text in the CC# field cause I cant read form labels. However, the error I got was stating the field was blank.
--Dave
Why use
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1" /> ?
I use:
<meta charset="utf-8">
But I'm not sure the necessity for either one.
The most productive function I've ever used --- I learned from ya --- structKeyExists --- TY Ray ---
:-)
Yah, I use it too. The only time I've had a problem with it was when using
<cfif structkeyexists(session,"foo")> and the session scope had expired. So I had to say
<cfif isDefined("session") and structkeyexists(session,"foo")>
Very cool Ray, thanks.
Now where can I get some of that pork loin rump turducken mentioned on the last page? Sounds delicious :)
+Dave - I fixed it in the blog text above, on the demo, and in the zip.
+Phillip - Dude, no idea. ;) It came from Bootstrap.
+Edward - You can, of course, also use isDefined.
+Phillip - Wow, what you described should never be possible. The session can't expire while the page is running. THe act of _hitting_ the page renews, or restarts, the session. If you can make a reproduceable case of this in CF9, let me know.
+SuperAlly - Thank Bacon Ipsum - http://baconipsum.com/
When the OnSessionEnd event fires, do you still have access to what was in the session scope or has it already cleared at that point?
See the article I linked to. But technically, yes. You do not have "the Session scope" as in, session.foo, but the method is passed a copy of it. So you have an argument that is a structure that you can "do stuff" with. Basically, read the article I linked to, then do a search for onSessionEnd in my blog.
@Ray - Thanks, sorry I missed that link up above. Exact answer I was looking for... time to go implement a feature!
Looks great as far as I got in the demo. I wasn't willing to put in a valid CC to test (I know...where's the trust?). I'll have to run it locally and check it out.
Paul, you can enter
4111111111111111
to test.
Dammit....now everyone has my CC...thanks Ray... ;)
Funny, I tried that originally, but must have miscounted my 1s. Thanks real slick, and I got to see the saved data part since I had already closed.
Thanks again Ray! This is similar to what I worked with you on about a month ago.
P
If someone is taking a long time to fill out a form (they go to lunch for instance), then when they press 'submit', their session has expired, and they are automatically considered logged out. If you assume a certain selection has been made to the session scope (session.shirtColor), then when the user presses submit, they get a login challenge & enter their credentials, but they have lost their session.shirtColor variable.
<cfif structkeyexists(session,"shirtColor")> will error. I suppose you could initialize session.shirtColor in onSessionStart.
Oh wait. That's not true.
I have to put myself in detention for the rest of the day.
Thanks for the "real world" example its great to get to see how the pro's would do it ! I've just finished a project using bootstrap for the backend admin and do think it has speeded up the layout process for alot giving a very professional feel to my pages :) agree with you about the lack of documentation. Having some more would have made it even quicker :0)
Great post.
I've taken to creating a server scope (which we could define global) function that gets the value of a structure if it exists otherwise returns a default value.
Instead of:
<cfif not structKeyExists(session, "checkout")>
<cfset session.checkout = {}>
</cfif>
You can do:
<cfset session.checkout = Server.GetStructValue(session,"checkout",{})>
Want to see an even sexier version using a function almost no one uses?
<cfset session.checkout = structGet("session.checkout")>
Great post!
Has anyone successfully implemented this into Mura CMS?
Where is the Zip filt? I could not see any.
How about something a bit more complex? For example, suppose the user has a cart full of items. Now before pulling up a list of all the items for him to pick from i want to ask him if he wants to just buy all the things in his cart? If he doesn't then i pull the list up and the user can pick and choose.
I can see how a step can be used to ask the user this question could be inserted, but that seems "wasteful" as in an extra cfm page where one is not needed.
So my question boils down to, how would one skip/process automatically a step based on a question asked before hand?
+Arthur - click the download link. It is at the end of my entry.
+Catdragon: You could just cflocation to N+1 instead of N. Unless I'm misreading your question, it's really not that complex.
Thanks Ray. For some reason (probably 'cause late in the day) it seemed awful complex. This morning, not so much. Thanks for the answer tho'.
This methodology for multistep forms looks great! I wonder if it would be possible to maintain this functionality with client variables instead of session variables although you would have to develop another method of storing struct as client vars can't handle those complex objects
You could use serializeJSON to store the values into the client scope and deserliazeJSON when you go to actually use em.
A dumb rookie question...
Since you are storing the structure in a session variable, isn't it necessary to put locks around it?
I feel dumb asking, but I thought that it was important to lock the session scope, especially on write. And if you were to save the struct to the client scope via serialized JSON, that would not be an issue.
Are there any advantages/disadvantages to saving the struct to the session vs the client scope?
Thanks!
Not a stupid question at all, especially since, for YEARS, we (CF folks in the community) said you had to use locks around ANY use of Session, Application, or Server variables.
Those locks were required before CF6 because of memory corruption issues.
Now they are only necessary in terms of race conditions.
However - you could only get a race condition in the example above if you were to open two (or more) tabs and tried to do form submissions at the same time. That's a rare situation and one a user would have to force upon himself, so it's not something I'd worry about here.
Make sense?
I kinda missed your last question. The Session scope is preferable here because we don't want it to last forever. Also, Session scopes take any type of data, not just simple values.
@Ray
Thanks! Both answers were very helpful!
The lock issues plague me. It like having Jiminy Cricket on my shoulder. I want to use a session variable in places, but then I worry about performance issues with locking that scope all over the app. But I also have concern about application instability resulting from not locking a session. So your insight about CF6+ was very helpful. If I keep that situation in mind, it should serve me well.
And the simple value limitation of client variables seems to be the biggest limitation. But that brings me to one more question. If I end up need to put my app in a CF cluster in the future, does the use of session variables present an issue? I read somewhere that it was much easier to work with clusters if the session scope was avoided and the relied on the client scope in its place.
Many thanks!
CF9 added session replication over a cluster, so in theory, it should "just work", but I'll be honest and say I've yet to try it.
The issue of session v. client is a very good question and requires more discussion as each route has its own implications. Ideally, session vars are ideal in that they are able to store structures and objects, where client based variables unless utilizing serializeJSON. (looking more into that route, thanks Ray)
Due to load-balanced server environment, we have been directed by our Server Admins to discontinue all usage of session variables and convert to client based. They support cf8 and cf9 for our environments at this time. Additionally, we have been directed to discontinue all absolute links and use only relative links based on the web file directory.
In the error checking on step 4 if the exp date is invalid (in the past) the error still says "Credit card number is empty or invalid."
Thanks Josh - I edited it. Not the nicest message, but better.
Hello Raymond,
just looking at your several examples concerning jquery and CF.
I just switched over developing a site with bootstrap... and get a bit more frustated due to the lot of scripting for clean form validation.
I did a research on cfuniform with bootstrap... but didn´t find it helpful...
Isn´t there a good way to have a cfc for bootstrap - forms? That´s what I´m searching for. Don´t want to code all the html-stuff again and again.
Maybe an xml-setup of a form and go... with clientside jquery and serverside validation too... maybe an expand of your posted jquery - validation step3. That would be a great shot.
Eastern was last week? Do you have some eggs for me?
Thanks for a suggestion to keep me on the way.
Are you asking about CF tags to generate HTML forms that work well w/ Bootstrap? If so, I'm not aware of any. I'm not really a fan of UI tags like that. You could create them yourself of course. :) CF custom tags are rather simple to create.
My business partners required 1998 DD 1857 last year and used a document
management site that has an online forms library . If others want 1998
DD 1857 too , here's or www.antipolo.gov.ph