Halhelms
SIGN UP FOR MY NEWSLETTER
 
 
Halhelms

Shameless Money

Recent Comments

Recent Entries

RSS

Subscribe

Form Processing : the OO Way

Apparently, the old saying is true: great minds think alike. Well, that may be giving me too much credit, but in a post last week by Ben Nadel here, Ben was continuing his exploration of object orientation.

I made a comment about how OO, though definitely not a magic bullet, could assist us in handling forms. Over the weekend, I started writing some code to explain this. The industrious Mr. Nadel came up with a solution.

I had in mind a more universal solution, which this post will detail.

Let's start with with a simple form. The form can either be called directly -- or it can be called from its corresponding CFC. Suppose we have a form called "MyForm" that looks like this:

<html>

<head>
   <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js">&</script>
   <style>
      .msg{color: red;}
   </style>
</head>

<body>

<form id="myform" method="post">
<div id="container_first_name">
<label for="first_name">First Name</label>
<input type="text" id="first_name" name="first_name" />
<p id="msg_first_name" class="msg"></p>
</div>

<div id="container_age">
<label for="age">Age</label>
<input type="text" id="age" name="age" />
<p id="msg_age" class="msg"></p>
</div>

<div id="container_favorite_color">
<label for="favorite_color">Favorite Color</label>
<select id="favorite_color" name="favorite_color">
   <option value="Red">Red</option>
   <option value="Blue">Blue</option>
   <option value="Black">Black</option>
   <option value="Green">Green</option>
</select>
<p id="msg_favorite_color" class="msg"></p>
</div>

<div id="container_languages">
<label for="languages">Languages Spoken</label><br />
<input type="checkbox" name="languages" value="English">English<br />
<input type="checkbox" name="languages" value="French">French<br />
<input type="checkbox" name="languages" value="Arabic">Arabic<br />
<input type="checkbox" name="languages" value="Mandarin">Mandarin<br />
<p id="msg_languages" class="msg"></p>
</div>

<div id="container_data_format">
<label for="data_format">Preferred Format (JSON, XML, or WDDX)</label><br />
<input type="text" id="data_format" name="data_format">
<p id="msg_data_format" class="msg"></p>
</div>

<input type="submit" value="OK">
</form>
<cfinclude template="MyForm.pgm">
</body>

</html>

(You can see this code running here.)

Apart from the CFINCLUDE of "MyForm.pgm" -- wel'll get to that in a moment -- it's a pretty standard form.

But we want some validation on the form. We can, of course, use standard client-side processing using jQuery or qForms or any other number of solutions. But these suffer from some problems:

(a) What if we have a constraint that the client can't know about -- usernames being unique, for example? That precludes a strictly client-side approach.

(b) We still need validation on the server, so any code we write for the client will be additional code.

We can solve these problems by moving validation to the server and using Ajax to do our form submission and alerting of any errors. To do this, each form will have its own corresponding CFC. Since we have "MyForm.cfm", we'll have a "MyForm.cfc" that looks like this:

<cfcomponent displayname="MyForm" output="false">

   <cfset variables.instance.first_name = {value = "", rules = {required = true, minLength = 3, maxLength = 20}}>
   <cfset variables.instance.last_name = {value = "", rules = {required = true, maxLength = 40}}>
   <cfset variables.instance.age = {value = "", rules = {datatype = "numeric", range="18,30"}}>
   <cfset variables.instance.favorite_color = {value = "", rules = {required = true}}>
   <cfset variables.instance.languages = {value = "", rules = {required = true, minList = 2}}>
   <cfset variables.instance.data_format = {value = "", rules = {required = true, allowed = "XML,JSON,WDDX"}}>
   
   <cfset variables.instance.errors = ArrayNew(1)>
   
   <cffunction name="display" access="remote" output="true">
      <cfinclude template="MyForm.cfm">
   </cffunction>
   
   <cffunction name="populate" access="remote" output="true" returnformat="json">
      <cfargument name="first_name" required="true">
      <cfargument name="age" required="true">
      <cfargument name="favorite_color" required="true">
      <cfargument name="languages" default="">
      <cfargument name="data_format" required="true">
      
      
      <cfloop collection="#arguments#" item="anArgumentName">
         <cfif StructKeyExists(variables.instance, anArgumentName)>
            <cfset variables.instance[anArgumentName]['value'] = arguments[anArgumentName]>
            <cfset this.validate(anArgumentName, variables.instance[anArgumentName])>
         </cfif>
      </cfloop>
      <cfif ArrayIsEmpty(variables.instance.errors)>
         
      <cfelse>
         
      </cfif>
      <cfreturn variables.instance.errors>
      
   </cffunction>
   
   <cffunction name="validate" access="public" output="false">
      <cfargument name="id" required="true">
      <cfargument name="fieldStruct" required="true">
      <cfset var response = {}>
      <cfloop collection="#arguments.fieldStruct.rules#" item="aRuleName">
         <cfset validation = application.validator.validate(aRuleName, fieldStruct.rules[aRuleName], fieldStruct.value)>
         <cfset response['valid'] = validation.valid>
         <cfif validation.valid EQ "false">
            <cfset response['msg'] = validation.msg>
            <cfset response['id'] = Lcase(id)>
            <cfset ArrayAppend(variables.instance.errors, response)>
         </cfif>
      </cfloop>
   </cffunction>
   
</cfcomponent>

Notice that the structure, "variables.instance" holds two keys: "value" and "rules". The "value" variable will be used to store the user responses; the "rules" variable is another struct with the rules we wish to apply to this particular field. Again, we'll see how these are put to use shortly. For now, just note that we can have a variety of rules and that any field can have zero or more rules applied to it.

Next, I've created an array called "variables.instance.errors". This will be used to collect any validation errors encountered and will, eventually, be sent back to the client.

The first method we encounter is "display". I mentioned above that the form could either be called directly or, as in this case, from the form CFC by calling its "display" method. The choice is a personal one; I don't see either as having a clear advantage over the other.

Next, we have a "populate" method. This method will be called when we want to submit the form. Of course, in "MyForm.cfm", there's no code to do submission. We'll handle that shortly in "MyForm.pgm". The "populate" method has arguments corresponding to the names of the forms. (To make this particular scheme work, the id of the field and the name of the field need to be the same.)

Inside "populate", I loop over the arguments. If a corresponding entry in "variables.instance" exists -- that is, if an argument named, say, "age" has a corresponding "variables.instance.age" entry -- we place the value of the argument in that structure. The value, of course, is the entry the user made in the form.

Next, we call the CFC's "validate" method, sending the entire structure entry for that argument. So, staying with our "age" example, we pass "variables.instance.age" which contains both the user's entry in that field and the validation rules that should be applied to this field.

I then have an empty "if" statement that checks to see whether there are any errors. Right now, it doesn't do anything -- we'll see shortly how this might be used. Finally, I return the entire "variables.instance.errors" array in JSON format.

So, what about this "validate" method? It's awfully short. That's because it uses an external validation class. The nice thing about this class is that it can be used for any sort of validation -- it has no idea that it's being used as part of this plan; it just does simple validation. Inside our "validate" method, we accept the "id" of the field (which matches the "name" of the field) and the "fieldStruct" -- the entry in "variables.instance" that corresponds with the field being validated. So with our "age" example, "id" would be "age" and "fieldStruct" would be the structure: .

A blank structure called "response" is declared and initialized. Then, I loop over the rules in "fieldStruct", calling that third-party validation class (I've set this up in my Application.cfc's "onApplicationStart" method to instantiate it in the application scope), using the validation class's "validate" method. That method is passed the "ruleName" (e.g. "datatype"), the arguments for that rulename (e.g. "numeric"), and the value the user entered.

The validation CFC returns a structure with two keys: "valid" (a Boolean) and "msg" (a string explaining the type of validation error). If validation produced a "valid" value of "false", we've encountered an error and the "response" structure is added to the variables.errors array.

Heading back to the "populate" method, we can see that after all validation processing is done, I've returned that errors array.

We're going to look at "MyForm.pgm" and see how all this integrates, but first, let's take a quick look at "Validator.cfc" -- that class responsible only for doing simple validation:

<cfcomponent displayname="Validator" output="false">
   
   <cffunction name="validate" access="remote" output="false" returnformat="json">
      <cfargument name="rule" required="true">
      <cfargument name="myRuleArgs" required="true">
      <cfargument name="myValue" required="true">
      <cfinvoke component="#this#" method="#rule#" returnvariable="response">
         <cfinvokeargument name="value" value="#myValue#">
         <cfinvokeargument name="ruleArgs" value="#myRuleArgs#">
      </cfinvoke>
      <cfreturn response>
   </cffunction>
   
   
   <cffunction name="required" access="public" output="false">
      <cfargument name="value" required="true">
      <cfargument name="ruleArgs" required="true">
      <cfset var response = StructNew()>
      <cfset response.valid = false>
      <cfset response.msg = "This field is required">
      <cfif Len(arguments.value)>
         <cfset response.valid = true>   
      </cfif>
      <cfreturn response>
   </cffunction>
   
   
   <cffunction name="minLength" access="public" output="false">
      <cfargument name="value" required="true">
      <cfargument name="ruleArgs" required="true">
      <cfset var response = StructNew()>
      <cfset response.valid = false>
      <cfset response.msg = "This field must be at least #ruleArgs# characters long">
      <cfif Len(arguments.value) GTE arguments.ruleArgs>
         <cfset response.valid = true>
      </cfif>
      <cfreturn response>
   </cffunction>
   
   
   <cffunction name="maxLength" access="public" output="false">
      <cfargument name="value" required="true">
      <cfargument name="ruleArgs" required="true">
      <cfset var response = StructNew()>
      <cfset response.valid = false>
      <cfset response.msg = "This field cannot be longer than #ruleArgs# characters">
      <cfif len(arguments.value) LTE arguments.ruleArgs>
         <cfset response.valid = true>
      </cfif>
      <cfreturn response>
   </cffunction>
   
   
   <cffunction name="datatype" access="public" output="false">
      <cfargument name="value" required="true">
      <cfargument name="ruleArgs" required="true">
      <cfset var response = StructNew()>
      <cfset response.valid = false>
      <cfset response.msg = "This field must be of type #ruleArgs#">
      <cfswitch expression="#arguments.ruleArgs#">
         <cfcase value="numeric">
            <cfif IsNumeric(value)>
               <cfset response.valid = true>
            </cfif>
         </cfcase>
      </cfswitch>
      <cfreturn response>
   </cffunction>
   
   
   <cffunction name="minList" access="public" output="false">
      <cfargument name="value" required="true">
      <cfargument name="ruleArgs" required="true">
      <cfset var response = StructNew()>
      <cfset response.valid = false>
      <cfset response.msg = "You must choose at least #ruleArgs# items">
      <cfif ListLen(arguments.value) GTE ruleArgs>
         <cfset response.valid = true>
      </cfif>
      <cfreturn response>
   </cffunction>
   
   
   <cffunction name="allowed" access="public" output="false">
      <cfargument name="value" required="true">
      <cfargument name="ruleArgs" required="true">
      <cfset var response = StructNew()>
      <cfset response.valid = false>
      <cfset response.msg = "Your entry must be one of these: #ruleArgs#">
      <cfif ListFindNoCase(arguments.ruleArgs, arguments.value)>
         <cfset response.valid = true>
      </cfif>
      <cfreturn response>
   </cffunction>
   
   
   <cffunction name="range" access="public" output="false">
      <cfargument name="value" required="true">
      <cfargument name="ruleArgs" required="true">
      <cfset var response = StructNew()>
      <cfset response.valid = false>
      <cfset response.msg = "Your entry must be between #ListFirst(arguments.ruleArgs)# and #ListLast(arguments.ruleArgs)#">
      <cfif arguments.value GTE ListFirst(arguments.ruleArgs) AND arguments.value LTE ListLast(arguments.ruleArgs)>
         <cfset response.valid = true>
      </cfif>
      <cfreturn response>
   </cffunction>
</cfcomponent>

This class is not dependent on any particular form: it has methods that can do a wide variety of validation -- and of course, you can add to the ones I've shown here.

Now, let's head over to "MyForm.pgm", which was included in "MyForm.cfm":

<cfajaxproxy cfc="MyForm" jsclassname="MyForm">
<script type="text/javascript">
      var myForm = new MyForm();
      myForm.setCallbackHandler(function(response){
         if (response.length){
            for (i=0; i<response.length; i++){
               $('#msg_' + response[i].id).html(response[i].msg);
            }
         } else{
            alert('Your form was processed');
         }
      });
      
      myForm.setErrorHandler(function(error){
         alert(error);
      });
      
      $('#myform').submit(function(){
         $('.msg').empty();
         myForm.setForm('myform');
         myForm.populate();
         return false;
      });
</script>

I begin by creating a CFAJAXPROXY to "MyForm.cfc". I instantiate that JavaScript class under the name "myForm"; it could be anything I choose. I set a callback handler -- a function that will be called when "MyForm.cfc" returns its response from the "populate" method. Inside this anonymous function, after checking that the array has some elements in it, I loop over the array, using jQuery to populate the various "msg" elements with the error message supplied to it by our Validator.cfc.

If there were no errors, I used an alert box.

Also, "myForm" has an error handler -- not very robust, but this is demo code.

How does the form get submitted? I begin by trapping the form's normal submission process. I then empty out anything in the various message elements. This is necessary in case validation failed and the user resubmits the form -- we don't want old error messages hanging around when they may no longer be valid.

I then call "myForm.setForm('myform');" This is a ColdFusion feature of the CFAJAXPROXY tag. It alerts ColdFusion that the next call to the "myForm" proxy should send all the form data inside the form with an id of "myform". That "next" call is immediate: "myForm.populate();". This automatically sends all form fields (with user-entered values) to the "populate" method, whose responsibilities we've seen before.

The final piece is to "return false" so that normal form processing is not attempted.

A couple of other points should be made.

1. I used a ".pgm" file. Why? I do this in order to separate the HTML markup from any processing that occurs. This isn't always possible -- if I were looping over a query, say, I'd have the CFLOOP on the .cfm page, but in general, it keeps a nice separation -- and makes it easier for designers to apply their design magic.

2. I have an empty CFIF based on whether the array is empty. In a real situation, this is where (if I were working with objects), I would instantiate the object and cause it to be persisted.

3. In a real environment, I wouldn't simply alert the user the form had been processed, but would take them to the appropriate page.

4. As some of these points illustrate, this is demo code. In a real situation, I'd need to "robustify" the code.

Apart from those, I'm pretty happy with this solution. What do you think?

Comments
Ben Nadel's Gravatar My only real concern is that you are defining the rules of the form in the Form object and then using those rules to validate the form.

Where does system-wide validation come into play (ie. username has to be unique, pre-requisite course must exist for curriculum selection)?

Also, by putting the rules in the form object, it seems that you decentralize the business logic. What if the MaxLength of a object property (yes yes, for later persistence) shortens from 50 to 30 and you have 3 different form interfaces that leverage this part of the model. You'd have to change the form rules in three different places.

While my solution doesn't have any of the nice display stuff or AJAX stuff (which is definitely nice), the validation logic was centralized in the service layer which means that it only had to be changed in one place (which may or may not require changes in the Form object depending on the severity of the change).
# Posted By Ben Nadel | 4/7/09 4:53 PM
Henry Ho's Gravatar Your example is long and hard to follow. Since all we care about is the OO architecture, and which component does what, maybe just use UML?

I still don't understand what's wrong with the view JS calling a remote CFC to validate something like unique username. IMO, it is the most pragmatic solution.
# Posted By Henry Ho | 4/7/09 5:11 PM
Hal's Gravatar @Ben System-wide validation occurs in the Validator CFC. For a specific validation like "username must be unique", I'd subclass the Validator and add this method in. And if you shorten the object property, yes, you'll need to change that wherever appropriate in the Form class. This is so infrequent a case that I'm not going to incur the additional abstraction costs of putting it in a service layer, but YMMV.

@Henry Well, actually, others are concerned with seeing the entire solution, not just the OO architecture. As for putting the validation code in the JS view and calling a remote CFC to validate, that's exactly what I'm doing. Sorry if the code is hard to follow. I tried to make it as clear as possible, but there are several moving parts and it does require a bit of study.
# Posted By Hal | 4/7/09 5:33 PM
Tony Garcia's Gravatar Thanks for taking the time to post and explain this, Hal. It's really helpful. (and don't speak for me, Henry)
# Posted By Tony Garcia | 4/7/09 6:17 PM
Ben Nadel's Gravatar @Hal,

Things that don't occur very often, like shortening a field length, might not be an issue. But, as the size of a web applications grows, I think we have all sort of agreed that the magic behind OO is the maintenance.

Remember, the biggest problem is not getting YOU to remember where things need to be changed - the biggest problem is YOU + 10 months remembering... or someone else alltogether. As such, while having to change validation logic in three places might not seem like much as the primary developer, it may start to be a problem as app or team starts to scale.

Although, maybe I'm making a completely wrong assumption - that you are using the Form object to validate your domain model as well??

If you domain model undergoes validation after the Form object, then I say we are fine. In that case, changing 2 out of 3 forms might throw an error once in a while, but we can iron those out as we find them (the same as we would do with any app).

But, if the application is relying on the Form object to ensure the integrity of the domain model... then I think we have a problem duplicating our validation logic in several places. This leaves us open to haveing corrupt domain models.
# Posted By Ben Nadel | 4/7/09 6:19 PM
Hal's Gravatar @Ben Ah, now I see where we were disagreeing! No, I'm not using the Form object for server-side validation. I am, however, using the Validation CFC for server-side processing. That is, I think, one of the advantages of having a separate Validator: it can be used both by the client and the server; no separate code is needed.
# Posted By Hal | 4/7/09 6:34 PM
Ben Nadel's Gravatar Ah, OK. That's the split we were making. So, after your form object handles all the validation, it presumably passing the information off to somewhere else that does additional validation in its own black-box sort of way.
# Posted By Ben Nadel | 4/7/09 6:55 PM
Brian Swartzfager's Gravatar While I'm personally not a fan of putting the validation error messages next to the individual fields (I simply prefer showing all the errors in one place), it does allow you to return generic error messages that aren't dependent on the context of the form field, so I may have to rethink that.

One type of validation that your example doesn't handle (in its current form) is conditional validation, like requiring an explanation if the user answered the previous question a certain way. But you could solve that by simply adding some custom code to the "populate" function after looping through the arguments.

It's certainly an interesting technique: thanks for sharing it!
# Posted By Brian Swartzfager | 4/7/09 7:25 PM
Hal's Gravatar @Ben Yes, that's it exactly.
# Posted By Hal | 4/7/09 7:49 PM
Aaron Longnion's Gravatar Complex, but creative and useful. And what if JS is disabled? It won't validate server-side. :-(
# Posted By Aaron Longnion | 4/8/09 5:00 AM
Hal's Gravatar @Aaron,

Ah, but that's the beauty! We can use the same Form.cfc's populate method and the same Validator.cfc to do server-side validation. We don't need separate code for client and server validations.
# Posted By Hal | 4/8/09 6:56 AM
anthony's Gravatar Maybe I'm missing the point of why we need to do client side validation, but I always did it because it saved me an http request in case something was wrong. Your method doesn't save me an http request, it adds another one. Additionally, since you are doing form validation using ajax before the submit and server side after the submit, you are actually doing the same validation twice. This seems a little redundant to me.
# Posted By anthony | 4/8/09 2:06 PM
Ben Nadel's Gravatar @Anthony,

Yes, it adds a request, but the request is quite fast (much faster than having to render an entire page response, HTML and all). So, even though there *is* a small additional HTTP overhead, the benefit you get is:

- Faster error checking
- Centralization of validation (no duplication on the client)
- Double-checking of validation

I would argue that the validation on the client AND the server is not redundant, but in fact, a benefit! We never want to rely on client-side validation alone; client-side validation is purely for richer user experience - its not mission critical. So, using this sort of a technique, we can create richer experiences without much additional overhead or redundancy.
# Posted By Ben Nadel | 4/8/09 2:16 PM
anthony's Gravatar @Ben
I know that you can't rely on client side validation, but does it make sense to validate client side, then immediately post the form and run the same validation?

I do my checking client side with just JS, no ajax requests, and the only thing that I don't get from your list of benefits is the centralization of validation. I actually get faster error checking because I don't have to make an ajax request.

Since all of Hal's rules are defined at the top of his MyForm object, couldn't he have a JSValidator javascript object that does the same thing as the validator object, but has rules passed into it in the MyForm objects display method? If you need to add or remove rules on a field, you still only change the rules in one central location. I do realize that this wont work for things like making sure usernames are unique though.
# Posted By anthony | 4/8/09 2:39 PM
Ben Nadel's Gravatar @Anthony,

I think there is definitely a way to create the Javascript validation based on the rules in the domain model... I'm just not sure how to do that yet :) Perhaps that is the next thing to explore.
# Posted By Ben Nadel | 4/8/09 5:33 PM
 
   
Clicky Web Analytics