For my final (well, for now) post on jQuery and forms validation, I thought I'd actually create a real form with actual back end processing. I'm going to demonstrate with a form that makes use of both client-side and server-side validation, and also demonstrate one of the cooler features of the jQuery Validation library - remote validation. Let's get started by describing our form and building it for entirely server-side validation. Imagine that I run a blog aggregator (oh wait, I do) and I want to make it easy for folks to send me information on their blogs so I can add it to the database. I'd need a form that asks for their blog name, URL, and RSS URL. (To be anal, I also use a description field at CFBloggers, but I'll keep it simple for now.) When not working within a framework like Model-Glue, I'll typically build a self-posting form (pseudo-code):
default form value
notice a form submission: create a list of errors if no errors, email, save to db, etc, and push to thank you page
display form: optionally display errors
Here is the initial version of the form with ColdFusion performing the validation server side. I assume none of this is unusual and since and the focus here is on jQuery I won't go over the code.
<cfparam name="form.blogname" default="">
<cfparam name="form.blogurl" default="">
<cfparam name="form.rssurl" default="">
<cfif structKeyExists(form, "save")> <cfset errors = > <cfif not len(trim(form.blogname))> <cfset arrayAppend(errors, "You must include a blog name.")> </cfif> <cfif not len(trim(form.blogurl)) or not isValid("url", form.blogurl)> <cfset arrayAppend(errors, "You must include a blog url.")> </cfif> <cfif not len(trim(form.rssurl)) or not isValid("url", form.rssurl)> <cfset arrayAppend(errors, "You must include a rss url.")> </cfif> <cfif arrayLen(errors) is 0> <cfmail to="email@example.com" from="firstname.lastname@example.org" subject="RSS Submission"> Blog Name: #form.blogname# Blog URL: #form.blogurl# RSS URL: #form.rssurl# </cfmail> <cflocation url="rssaddthanks.cfm" addToken="false" /> </cfif> </cfif>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Untitled Document</title> </head>
<h2>Add RSS Feed</h2> <form id="mainform" action="rssadd.cfm" method="post"> <fieldset>
<cfif structKeyExists(variables,"errors")> <b>Please correct the following error(s):</b><br/> <ul> <cfloop index="e" array="#errors#"> <li><cfoutput>#e#</cfoutput></li> </cfloop> </ul> </cfif>
<legend>Fill out the details of you blow below.</legend> <cfoutput> <p> <label for="blogname">Blog Name</label> <em></em><input id="blogname" name="blogname" size="25" value="#form.blogname#" /> </p> <p> <label for="blogurl">Blog URL</label> <em></em><input id="blogurl" name="blogurl" size="25" value="#form.blogurl#" /> </p> <p> <label for="rssurl">RSS URL</label> <em>*</em><input id="rssurl" name="rssurl" size="25" value="#form.rssurl#" /> </p> </cfoutput> <p> <input class="submit" type="submit" name="save" value="Submit"/> </p> </fieldset> </form>
Alright, so nothing too scary in there, right? You can demo this online here.
Let's add some jQuery love to the page. I'll begin by including my libraries of course:
Next I'll set up my validation and rules:
,blogurl: "required url"
,rssurl: "required url"
The details of how this works are described in my last entry, but basically I'm saying that all 3 fields are required and blogurl and rssurl also need url validation. (Hey IE folks, did I do my commas right?)
So, in English, this means that:
The name value will be required.
The blogurl value will be required, must be a URL, and the value will be passed to rssprocess.cfm and it must return true.
The rssurl value will be required, must be a URL, and the value will be passed to rssprocess.cfm and it must return true.
I'm using the same file to process both requests. I can do this because the plugin will send the name of the field as well. I could have used two different CFMs, or even two different CFC methods. Let's look at rssprocess.cfm:
<cfif structKeyExists(url, "blogurl")>
<!--- if blogurl, just do a check for status code 200 ---> <cfhttp url="#url.blogurl#" result="result"> <cfif structKeyExists(result.responseheader,"status_code") and result.responseheader.status_code is 200> <cfoutput>true</cfoutput> <cfelse> <cfoutput>false</cfoutput> </cfif>
<cfelseif structKeyExists(url, "rssurl")>
<!--- if blogurl, just do a check for status code 200 ---> <cftry> <cffeed source="#url.rssurl#" query="foo"> <cfoutput>true</cfoutput> <cfcatch> <cfoutput>false</cfoutput> </cfcatch> </cftry>
<cfelse> <cfoutput>false</cfoutput> </cfif>
I begin by turning on cfoutputonly. I'm not sure how well the plugin will handle values with whitespace around so it so I'm going to be anal about my output. I then check my URL scope. If blogurl was sent, I just do a HTTP check to ensure the URL exists. If rssurl was sent, I try to read it with cffeed and return true if the RSS feed can be parsed by CF. Notice that I return false in all error conditions, and if no value was passed at all. (Because people like me will run your site with Firebug, notice the Ajax requests, and try to run the file manually.)
You can demo this here. I also added custom messages. You can view source on the demo to see that. That's it. I don't think I'll write another form without jQuery validation in it!
Edit at 9:36AM CST Epic fail on my part. Thank you to Esmeralda for reminding me. I forgot to htmlEditFormat the form data to help prevent XSS type attacks. I normally do something like this in all my form checks:
<cfif not len(trim(form.blogname))>
<cfset arrayAppend(errors, "You must include a blog name.")>
<cfset form.blogname = htmlEditFormat(trim(form.blogname))>
Note the use of both trim and htmlEditFormat. Anyway, I've added it to all 3 dems, and thank you again Esmeralda for the reminder!