Form Handling and Validation with ColdBox, ColdSpring, and Transfer (Part 1)
The last time I blogged about this I got some great feedback, so I have gone back to it again and I am trying something new that combines some of the advice I have received from that post, and some advice from Brian Kotek.
The examples in this post use ColdBox, ColdSpring, and Transfer. I am sure that they would work just fine in other Frameworks using similar functions and techniques, so please read on.
This is an experiment. I am not touting it as a best practice. I am hoping to get feedback from the experts on this so I can improve or change where needed.
OK, now that the formalities are out of the way. Here is what I have:
This is a simple form in a ColdBox application that is used for creating calendar items with a name, start date, end date and other misc info.
And it is called via the URL:
http://mysite.com/?event=items.edit
That executes the event handler "items.edit()":
<cffunction name="edit" access="public" returntype="void" output="false">
<cfargument name="Event" type="coldbox.system.beans.requestContext" />
<!--- Get item service and an item Bean --->
<cfset var itemService = getPlugin("ioc").getBean("itemService") />
<cfset var itemBean = "" />
<cfset event.paramValue("itemid",0) />
<cfif NOT event.valueExists('itemForm')>
<cfset itemBean = itemService.getItem(event.getValue("itemid")) />
<cfset event.setCollection(itemBean.getMemento()) />
</cfif>
<cfset event.setLayout('Layout.Admin') />
<cfset event.setView('itemForm') />
</cffunction>
The event handler "edit()" firsts request the itemService from the ColdSpring Factory through the BeanFactory plugin in ColdBox. It then creates an empty variable to store the itemBean.
Next, the handler params a value for "itemid" in the Event object. If an itemid is not passed in, I will assume we are creating a new item. In my service layer, I have set "0" as my indicator for this.
Next, the handler checks to see if it (the handler) was called via the form page. If it was, there will be a hidden field called itemForm. If the hidden field is NOT there, then I know that the view is being visited for the first time and I need to get data from the persistence layer. So, I call upon my service layer (itemService) to get a bean with the appropriate data based on the itemid passed in.
If it does retrieve data from the persistence layer (Which is Transfer btw), then it uses ColdBox's event.setCollection() method to add the data from Transfer's getMemento() method into the event object.
Transfer's getMemento() method gets all of the properties of a bean and puts them into a struct. ColdBox's setCollection() methods takes in a struct and adds the values to the event object's values.
Finally, my handler sets the layout and the view to display.
So now let's look at the form.
The form view itemForm.cfm looks like:
<cfoutput>
<cfset WriteOutput(getPlugin("messageBox").renderit(true))>
<cfform name="itemForm" action="/index.cfm?event=items.editPost" method="post">
<cfinput type="hidden" name="itemForm" value="true" />
<cfinput type="text" name="itemid" value="#event.getValue('itemid')#">
<cf_label for="itemName" required="true" label="Name">
<cfinput type="text" name="itemName" size="40" maxlength="45" value="#event.getValue('itemName')#" />
</cf_label>
<cf_label for="startDate" required="true" label="Start Date">
<cfinput type="datefield" name="startDate" size="20" maxlength="12" value="#iif(isdate(event.getValue('endDate')), "dateFormat(event.getValue('endDate'), 'mm/dd/yyyy')", "")#" />
</cf_label>
<cf_label for="endDate" required="false" label="End Date">
<cfinput type="datefield" name="endDate" size="20" maxlength="12" value="#iif(isdate(event.getValue('endDate')), "dateFormat(event.getValue('endDate'), 'mm/dd/yyyy')", "")#" />
</cf_label>
<cf_label for="link" required="false" label="Link">
<cfinput type="text" name="link" size="60" maxlength="100" value="#event.getValue('link')#" />
</cf_label>
<cf_label for="descr" required="false" label="Description">
<cftextarea name="descr" cols="50">#event.getValue('descr')#</cftextarea>
</cf_label>
<cf_label for="btnSubmit">
<cfinput type="submit" name="btnSubmit" value="Add/Update" class="ajaxButton" />
</cf_label>
</cfform>
</cfoutput>
NOTE: Ignore the custom tag <cf_label>. It is a simple custom tag I am using for form layout
Now my form is based on the ColdBox event object. Some might argue with this, but for me, this is what feels right. The primary reason for this is because I can redisplay bad data that the end user has entered. Since the event object properties are not typed, as the bean/transferObject's are, I can leave the bad data in there. Whereas if I tried to base my view on a bean/TrO and someone put text into a numeric field, I would not be able to redisplay the text to the end user, because I would not be able to insert it into the bean/TrO.
This post is continued in part 2





Your choice to keep the data validation outside of the transfer object is also the way I handle my forms with the onTap framework. It's a pretty pragmatic approach actually, because then you get to redisplay the form to the user and say "hey, 'paris hilton' isn't a valid date" and they can look at the input element and see that "oh duh, I did enter 'paris hilton' in the date field".
Watch out.. because the memento implementation has changed in 1.1, so it could bite you.
Otherwise, keep up the interesting articles!
You're right, once I updated to 1.1, my application broke.
I guess I will need to figure something else out. Any suggestions from anyone else on how to handle this?
If you had renamed them, then you could loop over the structure, check the transfer object for a matching key that happens to be a function, i.e.
<cfset fname = "set" & rc[x] />
<cfif structKeyExists(to,fname) and isCustomFunction(to[fname])>
<cfinvoke component="#to#" method="#fname#">
<cfinvokeargument name="1" value="#rc[x]#" />
</cfinvoke>
Hope that reads okay. :)
Just catching up on my blog reading after a couple of weeks vacation and was excited to see your series. This is something that I'm very interested in, and am actually planning a few posts of my own on the subject. Anyway, regarding the question of an alternative to getMemento(), I have a copyToStruct() method in my AbstractTransferDecorator that does just this. I blogged about it some time ago at http://www.silverwareconsulting.com/index.cfm/2008.... I'm fairly sure that version of the method is up to date, even though the post is a bit old.