Object Oriented Form Helpers And Reusing Form Validation On The Client
After seeing Hal Helms' last blog post, I was inspired by the concept of leveraging Form helper objects in a way that allows us to reuse server-side validation code on the client. Because client-side validation needs to make an AJAX request to a CFC (so we don't need additional proxy files), it means that we only have one method call at our disposal. Based on that constraint, I went back and redesigned my previous Form helper object. This time, I created a base FormHelper.cfc that factors out much of the boiler plate, leaving it up to the sub-classes to define the critical logic.
To demonstrate the re-use of the validation logic, as well as the way it can be used for progressive enhancement of form validation, the following example uses a checkbox to determine the mode of form submission (AJAX vs. form post):
As you can see, the form works the same with either method of submission, the only difference being that one hijacks the form with jQuery-powered progressive enhancement techniques (with a few shortcuts taken that are not truly progressive enhancements).
I really liked the way Hal did a video code walk through, so I thought I would give it a try. I am not sure if it is adding value - I felt very rushed and I fear that made the code unclear (please let me know if you like this second video).
Ok, now that we have all the demonstrations out of the way, let's take some time to explore the code in-depth. First, let's look at the edit/add page so we can see the way in which our Form helper objects are being invoked:
<!--- Param ID. --->
<cfparam name="URL.id" type="numeric" default="0" />
<!--- Create a form utility. --->
<cfset objForm = CreateObject( "component", "SaveDateForm" ) />
<!--- Set form data. --->
<cfset objForm.SetFormData( FORM ) />
<!--- Create a default collection of errors. --->
<cfset arrErrors = [] />
<!--- Check to see if the form has been submitted. --->
<cfif objForm.Get( "submitted" )>
<!--- Set and process the form data. --->
<cfset objResponse = objForm.Process( FORM ) />
<!--- Check to see if the process was successful. --->
<cfif objResponse.Success>
<!--- The form went through, forward use to homepage. --->
<cflocation
url="index.cfm"
addtoken="false"
/>
<cfelse>
<!---
The form data was not valid and could not be process.
Store the erorr messages array into our local variable
for display.
--->
<cfset arrErrors = objResponse.Errors />
</cfif>
<cfelse>
<!--- Initialize the form data using the passed-in ID. --->
<cfset objForm.Init( URL.id ) />
</cfif>
<cfoutput>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Simple ColdFusion OOP Demo</title>
<script type="text/javascript" src="jquery-1.3.2.min.js"></script>
<script type="text/javascript" src="edit.js"></script>
<script type="text/javascript">
<!--- Check to see if we have any errors. --->
<cfif ArrayLen( arrErrors )>
$(
function(){
ShowErrors( #SerializeJSON( arrErrors )# );
}
);
</cfif>
</script>
</head>
<body>
<h1>
Add / Edit Date
</h1>
<form action="#CGI.script_name#" method="post">
<!--- Pass back submission flag. --->
<input type="hidden" name="submitted" value="true" />
<!--- Pass back ID. --->
<input type="hidden" name="id" value="#objForm.Get( "id" )#" />
<!--- Area to show error messages. --->
<div id="error-messages" style="display: none ;">
<p>
<strong>Please review the following:</strong>
</p>
<ul />
</div>
<p>
Girl:<br />
<input
type="text"
name="girl"
value="#objForm.Get( "girl" )#"
size="40"
/>
</p>
<p>
Activity:<br />
<input
type="text"
name="activity"
value="#objForm.Get( "activity" )#"
size="40"
/>
</p>
<p>
Date Occurred:<br />
<input
type="text"
name="date_occurred"
value="<cfif IsDate( objForm.Get( "date_occurred" ) )>#DateFormat( objForm.Get( "date_occurred" ), "mm/dd/yy" )#<cfelse>#objForm.Get( "date_occurred" )#</cfif>"
size="15"
/>
</p>
<p>
<input type="submit" value="Save Date" />
<label>
<input type="checkbox" id="use-ajax" />
<em>Use AJAX to process form</em>
</label>
</p>
</form>
</body>
</html>
</cfoutput>
One thing you might notice immediately is that I set the form data using the SetFormData() method before processing and then I set it again after I see that the form has been submitted. This is not the cleanest thing, and going back, I would probably remove the FORM argument from the Process() method. I need to set the form data using SetFormData() otherwise, I will never be able to check if the form has been submitted.
The Process() method returns a unified response whether it is called locally to ColdFusion or via a remote, AJAX request. The response objects has the following properties:
- Success - Boolean to flag success of failure of the request.
- Errors - Array of Field/Error structures.
- Data - Any data that needs to be returned as part of a successful request.
Because of this unified response object, the server-side processing adds a bit more overhead - the few lines of code needed to extract the errors from the response. But otherwise, the server-side processing is largely unchanged from my previous blog post.
Ok, now let's look at the upgraded Form helper object. When I went to rebuild it, I started to think about all the things that I could factor out. If this is a technique I am going to be using on various forms, I wanted a way to make the objects as small as possible. As such, I refactored the object and moved a lot of the "boiler plate" functionality into a base class, FormHelper.cfc:
<cfcomponent
outupt="false"
hint="I provide base form helper functionality.">
<!--- Create default instance structure. --->
<cfset VARIABLES.Instance = {
FormData = {}
} />
<cffunction
name="Commit"
access="public"
returntype="any"
output="false"
hint="I take the valiated form data and commit it to the system. If the commit fails, I will raise an exception.">
<!---
This is an abstract method meant to be overridden
by an extending class.
--->
<cfthrow
type="AbstractMethod"
message="You cannot access an abstract method."
detail="You cannot accesss an abstract method. You must override this method in an extending class."
/>
</cffunction>
<cffunction
name="Get"
access="public"
returntype="any"
output="false"
hint="I return the given form data value (or empty string).">
<!--- Define arguments. --->
<cfargument
name="Key"
type="string"
required="true"
hint="I am the form data key being fetched."
/>
<!--- Check to see if the form key exists. --->
<cfif StructKeyExists( VARIABLES.Instance.FormData, ARGUMENTS.Key )>
<!--- Return given value. --->
<cfreturn VARIABLES.Instance.FormData[ ARGUMENTS.Key ] />
<cfelse>
<!--- Key not found - return empty string. --->
<cfreturn "" />
</cfif>
</cffunction>
<!---
NOTE: This is name "Init" rather than initialize
because apparently "Initialize" is the name of a
built-in ColdFusion method.
--->
<cffunction
name="Init"
access="public"
returntype="any"
output="false"
hint="I initialize the form data.">
<!---
This is an abstract method meant to be overridden
by an extending class.
--->
<cfthrow
type="AbstractMethod"
message="You cannot access an abstract method."
detail="You cannot accesss an abstract method. You must override this method in an extending class."
/>
</cffunction>
<cffunction
name="NewError"
access="public"
returntype="struct"
output="false"
hint="This is a convenience method used to create an error struct based on field and error message.">
<!--- Define arguments. --->
<cfargument
name="Field"
type="string"
required="true"
hint="The field in error."
/>
<cfargument
name="Error"
type="string"
required="true"
hint="The error message to display."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Package the error message into a struct. --->
<cfset LOCAL.Error = {
Field = ARGUMENTS.Field,
Error = ARGUMENTS.Error
} />
<!--- Return the error message. --->
<cfreturn LOCAL.Error />
</cffunction>
<cffunction
name="Process"
access="remote"
returntype="struct"
output="false"
hint="I process the form data and returning a unified response object that can be used locally or remotely.">
<!---
The arguments will come through as a set of form
elements OR as a single structure. Either way, we
want to package them such that we can treat them
as a structure.
--->
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Create a default response object. --->
<cfset LOCAL.Response = {
Success = true,
Errors = [],
Data = ""
} />
<!---
Set the form data. Check to see if we were given a
single struct or a set of properties.
--->
<cfif IsStruct( ARGUMENTS[ 1 ] )>
<!--- Treat first argument as the FORM scope. --->
<cfset THIS.SetFormData( ARGUMENTS[ 1 ] ) />
<cfelse>
<!---
Form data elements were passed individually.
Therefore, treat ARGUMENTS scope as if it were
FORM scope.
--->
<cfset THIS.SetFormData( ARGUMENTS ) />
</cfif>
<!---
Validate the form data and store the error messages
into our reponse errors.
--->
<cfset LOCAL.Response.Errors = THIS.Validate() />
<!---
Check to see if we have any error messages. If we
do, then our form processing was not successful and
we need to update the success flag.
--->
<cfif ArrayLen( LOCAL.Response.Errors )>
<!--- Flag as invalid form data. --->
<cfset LOCAL.Response.Success = false />
<cfelse>
<!---
The form data has been validated. Now, let's
commit it to the system.
CAUTION: If the commit fails, it will raise an
exception that we must catch.
--->
<cftry>
<!---
Commit the form data and store the response
in our Data field.
--->
<cfset LOCAL.Response.Data = THIS.Commit() />
<!--- Catch commit errors. --->
<cfcatch>
<!---
Something went wrong, but we can't know
what it was - it was unforseen. We can
only return a generic error message.
--->
<cfset LOCAL.Response.Success = false />
<!--- Set generic error message. --->
<cfset LOCAL.Response.Errors = [
{
Field = "",
Error = "An unknown error has occurred."
}
] />
</cfcatch>
</cftry>
</cfif>
<!--- Return the form procesing response. --->
<cfreturn LOCAL.Response />
</cffunction>
<cffunction
name="SetFormData"
access="public"
returntype="void"
output="false"
hint="I add the form scope to the instance variables.">
<!--- Define arguments. --->
<cfargument
name="FormData"
type="struct"
required="true"
hint="I am the form data collection being processed."
/>
<!--- Add the form scope to the instance variables. --->
<cfset StructAppend(
VARIABLES.Instance.FormData,
ARGUMENTS.FormData
) />
<!--- Return out. --->
<cfreturn />
</cffunction>
<cffunction
name="Validate"
access="public"
returntype="array"
output="false"
hint="I validate the form data and return an array of error messages.">
<!---
This is an abstract method meant to be overridden
by an extending class.
--->
<cfthrow
type="AbstractMethod"
message="You cannot access an abstract method."
detail="You cannot accesss an abstract method. You must override this method in an extending class."
/>
</cffunction>
</cfcomponent>
This base class handles the data in a generic way, including creating the unified response object returned from the Process() method. Notice that Process() method's access is set to "remote." This allows us to instantiate this object and call this method via an AJAX request. This remote Process() method serves only as the gatekeeper to the form processing - the real "meat" of the processing is handed off to the sub-classes which are responsible for overriding the following abstract methods:
- Commit()
- Init()
- Validate()
This allows us to keep the actual form-specific components as lean as possible. The following component, SaveDateForm.cfc is a sub-class specific to our date form that extends our base FormHelper.cfc:
<cfcomponent
extends="FormHelper"
output="false"
hint="I am the form utility for saving dates.">
<!---
Store the service object that we will need to communicate
with. This is a VIOLATION of encapsulation, but since this
is an extension of the "Controller" that needs to have
REMOTE ACCESS (that cannot be initialized properly), I am
going to consider this ok.
--->
<cfset VARIABLES.Instance.DateService = APPLICATION.DateService />
<!--- Set up default form variables. --->
<cfset VARIABLES.Instance.FormData = {
id = 0,
girl = "",
activity = "",
date_occurred = "",
submitted = false
} />
<!--- ------------------------------------------------- --->
<cffunction
name="Commit"
access="public"
returntype="numeric"
output="false"
hint="I take the valiated form data and commit it to the system and return the ID of the new date object. If the commit fails, I will raise an exception.">
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Create a new date object. --->
<cfset LOCAL.Date = CreateObject( "component", "Date" ).Init(
ID = VARIABLES.Instance.FormData.id,
Girl = VARIABLES.Instance.FormData.girl,
Activity = VARIABLES.Instance.FormData.activity,
Date = VARIABLES.Instance.FormData.date_occurred
) />
<!--- Save the date. --->
<cfset VARIABLES.Instance.DateService.SaveDate( LOCAL.Date ) />
<!--- Return the ID of the new date. --->
<cfreturn LOCAL.Date.GetProperties().ID />
</cffunction>
<cffunction
name="Init"
access="public"
returntype="any"
output="false"
hint="I initialize the form data.">
<!--- Define arguments. --->
<cfargument
name="ID"
type="numeric"
required="true"
hint="I am the ID of the date with which we are initializing the form."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!---
Try to get the existing date (might throw
exception if date doesn't exist).
--->
<cftry>
<cfset LOCAL.Date = VARIABLES.Instance.DateService.GetDateByID( ARGUMENTS.ID ) />
<!--- Get the date properties. --->
<cfset LOCAL.Properties = LOCAL.Date.GetProperties() />
<!--- Move date propreties into form. --->
<cfset VARIABLES.Instance.FormData.id = LOCAL.Properties.ID />
<cfset VARIABLES.Instance.FormData.girl = LOCAL.Properties.Girl />
<cfset VARIABLES.Instance.FormData.activity = LOCAL.Properties.Activity />
<cfset VARIABLES.Instance.FormData.date_occurred = LOCAL.Properties.Date />
<!--- Catch any errors. --->
<cfcatch>
<!--- Date did not exist. --->
</cfcatch>
</cftry>
<!--- Return out. --->
<cfreturn />
</cffunction>
<cffunction
name="Validate"
access="public"
returntype="array"
output="false"
hint="I validate the form data and return an array of error messages.">
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Set up the array of error messages. --->
<cfset LOCAL.ErrorMessages = [] />
<!--- Get the raw errors. --->
<cfset LOCAL.Errors = VARIABLES.Instance.DateService.ValidateDateProperties(
ID = THIS.Get( "id" ),
Girl = THIS.Get( "girl" ),
Activity = THIS.Get( "activity" ),
Date = THIS.Get( "date_occurred" )
) />
<!--- Validate girl. --->
<cfif StructKeyExists( LOCAL.Errors, "Girl" )>
<cfset ArrayAppend(
LOCAL.ErrorMessages,
THIS.NewError(
Field = "girl",
Error = "Please enter a girl name."
)
) />
</cfif>
<!--- Validate activity. --->
<cfif StructKeyExists( LOCAL.Errors, "Activity" )>
<cfset ArrayAppend(
LOCAL.ErrorMessages,
THIS.NewError(
Field = "activity",
Error = "Please enter an activity."
)
) />
</cfif>
<!--- Validate date. --->
<cfif StructKeyExists( LOCAL.Errors, "Date" )>
<cfif (LOCAL.Errors.Date.Type EQ "InvalidProperty.Type")>
<cfset ArrayAppend(
LOCAL.ErrorMessages,
THIS.NewError(
Field = "date_occurred",
Error = "Please enter a valid date."
)
) />
<cfelseif (LOCAL.Errors.Date.Type EQ "InvalidProperty.FutureDate")>
<cfset ArrayAppend(
LOCAL.ErrorMessages,
THIS.NewError(
Field = "date_occurred",
Error = "Please enter a date in the past (cannot store future dates)."
)
) />
</cfif>
</cfif>
<!--- Return error messages. --->
<cfreturn LOCAL.ErrorMessages />
</cffunction>
</cfcomponent>
As you can see, this has not changed drastically from my previous attempt; it still hands the validation off to the service layer and then translates data type errors into user-friendly errors to be displayed on the front-end.
The last piece of the puzzle is the Javascript behind the edit-page. As you will see below, the edit-page Javascript is quite small and consists of not much more than a jQuery AJAX call and some utility methods:
// Create a mini jQuery plugin.
jQuery.fn.input = function( strInputName ){
// Return the first input with the given name.
return( this.find( ":input[ name = '" + strInputName + "' ]" ) );
}
// I populate and display the errors area with the given
// collection of errors.
function ShowErrors( arrErrors ){
var jErrors = $( "#error-messages" );
var jErrorsList = jErrors.find( "ul" ).empty();
// Loop over the errors and populate.
$.each(
arrErrors,
function( intI, objError ){
jErrorsList.append(
"<li>" +
objError.ERROR +
"</li>"
);
}
);
// Show errors.
jErrors.show();
}
// Submit the form via AJAX.
function SubmitForm( jForm ){
$.ajax(
{
type: "post",
url: "SaveDateForm.cfc",
data: {
method: "Process",
returnformat: "json",
// Form data.
id: jForm.input( "id" ).val(),
girl: jForm.input( "girl" ).val(),
activity: jForm.input( "activity" ).val(),
date_occurred: jForm.input( "date_occurred" ).val(),
},
dataType: "json",
// Success handler.
success: function( objResponse ){
// Check to see if response is successful.
if (objResponse.SUCCESS){
// Redirect user.
window.location.href = "index.cfm";
} else {
// Show error messages.
ShowErrors( objResponse.ERRORS );
}
}
}
);
}
// Run when the DOM can be interacted with.
$(
function(){
var jForm = $( "form:first" );
var jAJAXCheckbox = $( "#use-ajax" );
// Hijack form submission.
jForm.submit(
function( objEvent ){
// Check to see if we are using AJAX form submission.
if (jAJAXCheckbox[ 0 ].checked){
// Submit the form via AJAX.
SubmitForm( jForm );
// Return false to prevent default form action.
return( false );
} else {
// AJAX checkbox was not checked. Allow the
// standard form submission to take place.
return( true );
}
}
);
}
);
Because the Process() method returns the same unified response object for an AJAX request or a local ColdFusion request, dealing with it in Javascript is equally easy; all I have to do is check the SUCCESS flag and hand off any errors that are returned.
Until I explored the concept of reusing validation on the client-side, I really didn't feel that the Form helper object added much value. But, now that I see how it can be leveraged by both the server and the client in a progressive enhancements fashion, I have to say that it seems much more interesting.
Want to use code from this post? Check out the license.
Reader Comments
Ben, this is some seriously cool OOP! I'm not familiar with JSON, but from what I do know, it's a very good way to move data around with AJAX, so I have a question....
In your AJAX call, you specify the return type as JSON, but in your Process() method in the CFC, the return type is STRUCT. Does this conversion always work automatically? Is anything else necessary in order to return a structure as JSON?
@Eric,
With a "Remote" access CFFunction, if you call it locally (within ColdFusion), then it uses the standard return type (ex. struct). If you access this remote method via AJAX, however, it will take that "struct" and, by default, convert it to WDDX to be returned in the AJAX call.
You can provide a different default conversion if you provide the ReturnFormat attribute in the CFFunction tag:
<cffunction access="remote" returnformat="json">
That ReturnFormat attribute is merely a suggestion. You can override it in the AJAX request (as I do with my returnFormat AJAX property). You have the choice of using:
wddx (default)
plain
json
Glad you liked the example :)
That is really good to know. I'm definitely saving this one for future use. Thanks.
OK.. for a simple form the OO concept seems useful. Now what do you do with a complex form. This is a real form not simulated for debate. :)
1. You have a caller and based on the line called in different questions are asked.
2. You have products (order) and therefore also order items.
3. You have credit card charges "if" there is a dollar amount.
I am curious how you handle this type of thing when you have a layerd complexity. Hal's example makes sense in the simple world but it seems like it may turn into an ANTI-Pattern for a more complex form. (Before anyone starts defending... I AM NOT saying Hal did it wrong... rather questioning how it would be applied to something more complex.)
(Perhaps you should create an app where you get long term serious with a date and you now need to get to know inlaws because you are visiting during a holiday and need to remember family details!)
@John,
At its most basic sense, we just taking the form validation that we would do procedurally and move it into a Validate() method in the form helper. So, theoretically, anything we could do with procedural validation we could do in the Validate() method.
The *real* question is, do we ever need to validate in such a way that passing back an array of error fields/messages is not sufficient feedback for the client?
That is a much more interesting question, I think speaks more to your questions about complexity. In order to allow for this, my collection of errors contain a Field and an Error. Therefore, it would allow the client to target and highlight individual fields and act on a field-by-field basis.
To be honest, I don't think I've ever really built a form that that was so complex that it didn't operate off of a collection of error messages (using procedural programming).
I am not saying that you are off base in anyway! I am admitting that my forms, to date, have not been very rich / complex, and I'm having trouble thinking outside the box.
If you can come up with a good, concrete problem, I would love to explore this.
You want to add a Form Helper layer to validate FORM data before creating the actual domain object, is that all?
@Henry,
With the Form helper or not, I would still be validating the data before creating a domain object. In fact, I would only ever create a domain object if the data was valid (at least minimally).
The Form helper gives us the ability to add pain-free client-side validation without having to duplicate validation.
VERY nice shiznit working both sides of the server. Mucho kudos on ya Ben. Im not familiar with anything else but jsVal framework which is only clientside and this look quite nice.
Im no CF person, and the classes/syntax is pretty greekish to me, but I think Im a gonna swipe your delicious and tasty concept/solution/pattern and port over to asp classic where it would make-a-more sense to my asp prog skill set when it comes time to make changes.
I can't immediately see any deterrent to multipart forms, which is pretty standard in my neck of the server woods.
Thanks for the informative post
@Dave,
Glad you like. A multi-part form (file upload) would definitely complicate things because a file cannot be uploaded via AJAX. In that case, you might need to submit the form via an IFrame or something for validation.
Post back here if you have anything we should check out.
Ben, thanks for the heads up on that. In the case of multi-parts, Ill just disable ajax validation for it. No whoop as that's NOT available currently and won't be missed. ;o)~ For the single page forms this will be great....
...Well, as soon as I get my CF class decoder ring in the mail and get it working, that is. tnx again.
Sorry to usurp your comment's Ben. Just a revisit/renote note for asp classic/asp.net peops --
the OWASP (Open Web Application Security Project) series of open sauce projects already has a validation framework written for asp classic & .net languages. (in addition to MANY others) It looks pretty nice actually. In addition to validation, that they also have a full security framework for web apps that goes way beyond forms validation. url:
https://www.owasp.org/index.php?title=Category:OWASP_Enterprise_Security_API&setlang=es
Don't know how I missed it in my search that brought me to this nice solution of Ben's looing for Asp classic validation--- but hey, I do live in a cave and have very translucent pink skin, maybe that's part of it? ;o)~
oWASP also have a whole nebula of sub projects in various languages and is def worth looking into:
http://www.owasp.org/index.php/Category:OWASP_Project
I did see rumors of a CF version that might be in the works listed in a couple of places, but i didnt find direct links to that project's existence. However this line is in the text at one point:
"The Java version is callable from ColdFusion":
http://www.owasp.org/index.php/Category:OWASP_AntiSamy_Project
Again, to Ben, thanks for sharing your nice idea/solution. Well done, mate.
@Dave,
No problem at all my man; I'm always in favor of getting good information out there :)
Hi Ben
I'm trying to follow through your example and am missing a function somewhere.
In your SaveDataForm.cfc you reference a function called ValidateDateProperties() on about line 136:
<cfset LOCAL.Errors = VARIABLES.Instance.DateService.ValidateDateProperties(...)
I can't seem to find it. Can you help me.
Cheers
Marty
Thanks for posting this. I took me a while to find my way through, but this is a nice and clean setup. Once you have everything figured out :-)
Now I would have a question, too:
Say I have a form (and corresponding database table) with 20 fields, but depending on the situation, only some of the fields are required and submitted (registration, login only, configure user settings).
If I follow your example and define the form fields inside my saveFormDates.cfc in VARIABLES.Instance.FormData and then in setFormData() append the submitted values, I will only overwrite the default Instance values if a value was submitted. Unsubmitted form fields would still be at their default value (="") and passed into validation, too.
Question:
Say I store default values for any given form into Session or Application Scope, as they probably never change. Then I could setup the instance from the Application Scope and when a form is submitted, I would overwrite the instance vs. appending to it. This way only the submitted fields get validated and when committing I can still append validated fields to the Session Scope to pass empty values for all unfilled fields. Would that make sense (also encapsulation- and object-oriented wise)?
A little like so:
Doing it this way, saveFormDates would also be flexible to manage all table-related forms, so I would end up with a form-helper.cfc and extended cfcs more or less corresponding to my database structure.