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

Comments
ike's Gravatar I would recommend instead of using event.setCollection, to use structAppend(event.getCollection(),itemBean.getMemento(),false) -- this would allow you to load the request with URL parameters without having them erased or overwritten by the object's memento.

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".
# Posted By ike | 8/15/08 10:28 PM
Mark Mandel's Gravatar Is this where I point out that getMemento() is unsupported? ;o)

Watch out.. because the memento implementation has changed in 1.1, so it could bite you.

Otherwise, keep up the interesting articles!
# Posted By Mark Mandel | 8/18/08 9:21 PM
Jason Dean's Gravatar @Ike, Thanks for the comments and suggestion. Also for the validation in how I am doing this.
# Posted By Jason Dean | 8/18/08 10:21 PM
Jason Dean's Gravatar @Mark - Thanks for the info on set/getMemento(). I am disappointed to read that, I really liked the methods.

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?
# Posted By Jason Dean | 8/18/08 10:23 PM
ike's Gravatar I don't know if Transfer includes a method of fetching the column info for your table... If not, then if you can get the name of the table from your transfer object, you should be able to use cfdbinfo to get column names and then you can use that query to determine which struct keys qualify to be set in your transfer object, assuming you haven't renamed them...

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. :)
# Posted By ike | 8/18/08 10:44 PM
Bob Silverberg's Gravatar Hi Jason,

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.
# Posted By Bob Silverberg | 8/19/08 9:57 PM
BlogCFC was created by Raymond Camden. This blog is running version 5.9.1. Contact Blog Owner