Five Stars! Add a Rating Widget to your ColdFusion App
ColdFusion 9 – The latest version – has just recently been released as a beta preview on Adobe Labs. ColdFusion is Adobe’s server-side platform for web application development and we’re jumping on this opportunity to use it in combination with the brand-new ColdFusion Builder. We’ll develop a rating widget, so that you can provide your users with the option to rate content on your web site. The source code for this article is also available for download.
Once you’ve read the article, why don’t you test your knowledge with our quiz? If you’re one of the first 200 to complete the quiz, Adobe will send you a printed copy of the Adobe ColdFusion Evangelism Kit.
Before we start let me say that this tutorial assumes that you’ve installed a stand-alone ColdFusion 9 server on your local development machine in a non-J2EE deployment and that this server is running on port 8500. If you have set up your server in a way to use a locally installed HTTP server like Apache or IIS, you might need to place the files I’m mentioning in a different location on your hard drive, as well as change all port references to port 80.
Setting Up The Database
All right, let’s start! Our task is to add a rating widget to a ColdFusion application or page. Therefore, we need to have a ColdFusion application first, and the easiest way to achieve that is by creating a few database tables to store content and ratings. If you’ve followed Kay Smoljak’s tutorial on building a URL shortener application, the easiest way is to just add a rating table to the existing Derby database. You can do so by executing the sql/db.cfm
file of the download package for this tutorial.
For those users of SQL Server, we aim to keep this tutorial as straightforward as possible. So rather than dive into the depths of database schemas, here’s a screenshot of the schema diagram and how the two tables are related.
Database schema for content and rating table (View larger image in a new window.)
SQL scripts that create the two tables on SQL Server 2005 or 2008 are part of the download package (you’d have to create the Content table first, as both scripts assume there’s a database named “sitepoint” already existing on your SQL Server). Keep in mind that this is just an example; the column definitions of the Content table are arbitrarily chosen and the only requirement is really to have a unique content identifier that can be tied into the rating table to associate both entities.
Importing the Project into ColdFusion Builder
In Kay Smoljak’s and my article “What’s new in ColdFusion 9“, we’ve discussed a variety of the new features in CF9. Among those is the aforementioned ColdFusion Builder. The code samples for this tutorial come as an exported CF Builder project and can be imported into your own project using the import wizard in the File menu of CF Builder. The image below shows the project’s file and folder structure.
CF Builder project structure
Displaying the Content
The first file we’re going to look at is index.cfm
. Let’s pretend this is the main application file that’s supposed to query and show the content:
...
<cfscript>
oContent = CreateObject("component","nz.co.ventego.sitepoint.Content");
data = oContent.getAllContent();
</cfscript>
<body>
<cfoutput query="data">
<h3>http://#cgi.server_name#:#cgi.server_port#/URLS/?#data.shortlink# (id: #data.id#)</h3>
<p>(#data.link#)</p>
<p>
<strong>Do you like this content? Why not rate it...</strong><br/>
</p>
<br/><br/>
<hr/>
</cfoutput>
</body>
...
The top <cfscript>
block instantiates a CFC (ColdFusion Component) and calls the method getAllContent
. The method executes an SQL query against our database and retrieves a list of all Content entries, together with the amount and the average value of ratings (we’re allowing users to pick a score between one and five stars):
SELECT C.id, C.label, C.text, C.link, C.shortlink, AVG(R.rating) as average_rating, COUNT(R.id) as number_of_ratings
FROM Content C
LEFT JOIN Rating R ON C.id = R.contentId
GROUP BY C.id, C.label, C.text, C.link, C.shortlink
The resultset
of this query is stored in the variable data on the page and can now be used there. The <cfoutput>
tag’s query
attribute loops over the query resultset
and for each row of the query, the HTML content from <h3>
to <hr>
is created, as shown below.
Database output (View larger image in a new window.)
While looping over the resultset
ColdFusion will evaluate the expressions that are embedded between the #-symbols and replace those with data from the resultset
. As you can see in the code, we’re using the shortlink
and link
columns from Kay’s tutorial to rate the shortened URLs her application created. Let me just point out that the Content table has columns to store a label and text as well – so feel free to store whatever data you like in there, and change the display logic shown above in index.cfm
to tweak the output for your needs.
The Custom Tag
The rating widget will be implemented as a ColdFusion custom tag. Custom tags were introduced into ColdFusion back in the days of ColdFusion 3 or 4 but are still the most efficient and preferable way to create modularized functionality for the direct purpose of creating page output. From a high-level point of view a custom tag is nothing more than including a .cfm
file into another .cfm
file, but we’re in fact following a few guidelines and requirements in writing and accessing this custom tag.
A CFML custom tag can be called via <cfmodule>
and gets passed so-called custom tag attributes by specifying key-value pairs. Our rating widget will need to be passed three pieces of information: the content identifier it’s supposed to be tied against, the average current rating, and the amount of ratings that we’ve already received for this content. We’ll place the following line right after the <strong>...</strong><br/>
block in index.cfm
later to embed our widget:
<cfmodule template="customtags/widget.cfm" contentId="#data.id#" rating="#data.average_rating#" ratingcount="#data.number_of_ratings#"/>
The widget code itself is placed in the subfolder customtags
and named widget.cfm
. The latter indicates again that our custom tag is simply a normal .cfm
template written in a specific way. The custom tag itself is surprisingly concise, partly due to the fact that we’re leveraging the Ajax framework, Spry to help us with managing the rating mechanism itself. Let’s have a look at the custom tag first:
<cfif thisTag.executionMode eq "start">
<cfif isDefined("attributes.contentId")>
<cfoutput>
<p>
<cfif isDefined("attributes.rating") AND isDefined("attributes.ratingcount")>
<div>The current score is #DecimalFormat(attributes.rating)# based on #attributes.ratingcount# votes.</div>
</cfif>
<span id="rate_content_#attributes.contentId#" class="ratingContainer">
<span class="ratingButton"></span>
<span class="ratingButton"></span>
<span class="ratingButton"></span>
<span class="ratingButton"></span>
<span class="ratingButton"></span>
<span class="ratingRatedMsg thankyou">Thanks for voting!</span>
<input type="text" id="ratingValue_#attributes.contentId#" name="ratingValue_#attributes.contentId#" value="#attributes.rating#" />
</span>
<script type="text/javascript">
var rate_content_#attributes.contentId# = new Spry.Widget.Rating("rate_content_#attributes.contentId#", {allowMultipleRating:false, ratingValueElement:"ratingValue_#attributes.contentId#", postData:"contentId=#attributes.contentId#&rating=@@ratingValue@@", saveUrl:"rate.cfm"});
</script>
</p>
</cfoutput>
<cfelse>
<p>Please supply a contentId value to use this widget for rating content</p>
</cfif>
</cfif>
A custom tag has different execution modes. The idea behind this concept is that one should be able to “wrap” a custom tag with opening and closing tags around other existing content on a page and apply whatever functionality the tag provides to the enclosed content. In our case, we just want to call the custom tag once to actually include the rating widget and attach it to an item on the page. At the beginning of the custom tag, we’re therefore just checking if the execution mode of this tag is “start”. If the tag is used in any other mode, no output is created and the custom tag has no effect.
Everything that’s been passed into the custom tag from the index.cfm
via the <cfmodule>
tag ends up in the so-called “attributes scope” within the custom tag. We’re checking if the contentId
has been passed in and therefore if the variable attributes.contentId
is defined. If that’s the case we can at least safely run the rating widget. If the developer using our widget.cfm
passes in both a value for attributes.rating
and attributes.ratingcount
, we’ll display information of the current average score and the amount of votes for that content item.
Adding Ajax with Spry
After this conditional CFML code, the real rating widget is being set up. As mentioned earlier, we’re going to use some widgets from an Ajax library called Spry. Spry is a lightwight collection of widgets, effects, and other useful Ajax-related classes developed by Adobe that’s been released under a BSD license. The nested set of <span>
tags creates the rating widget itself and five elements of class ratingButton
inside. We also throw in a message to be displayed after the user has voted and an <input>
element to store the current rating. The latter will allow our rating widget to reveal the average score (in stars) at that point when it’s loaded (see the figure below). It’s worth mentioning that Spry is obviously not the only way of creating a dynamic and Ajax-based widget; other JavaScript frameworks such as jQuery or extJS are also able to provide a solution.
Preloaded “stars” to reflect the average score
Below the section of nested <span>
tags we’re actually instantiating an object of the Spry.Widget.Rating
class in JavaScript. In our scenario we’re going to have multiple content items with a rating widget each on a single page, so we’ll have to make sure that we’re creating individual instances of the Rating
class. This is done by naming the instances rate_content_#attributes.contentId#
– the ColdFusion expression in #’s is evaluated on the server before the HTML is delivered to the client and the JavaScript is executed there. The constructor of the JavaScript object is passed a reference to the id
of the <span>
section we created just before, as well as an object of properties and settings.
The latter is of particular interest as it allows us to customize the Spry widget in a variety of ways. In this case here, we allow a user to vote just once while being on the page (without additional checks on the server they can vote again when they reload the page) and we define a saveUrl
in combination with a postData
query string. As soon as a user of the widget clicks on a star, we’re sending the current contentId
and the selected rating to the rate.cfm
template via a POST
request.
Data that has been sent to a CFML page via a POST
request becomes variables in the form scope; then the core task of rate.cfm
is to check if contentId
and the rating are actually defined in the latter, before executing a SQL statement to insert the new rating into the Rating table. For this data access, we’re following a similar pattern to what we used in the data retrieval before:
- Create an instance of a data access CFC:
oRating = CreateObject("component","nz.co.ventego.sitepoint.Rating");
- Call a method of the previously instantiated component:
oRating.insertRating(form.contentId,form.rating);
The Rating.cfc
‘s insertRating
method executes the following SQL query:
<cfquery datasource="sitepoint" name="local.qRating">
INSERT
INTO Rating(contentId,rating)
VALUES (<cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.contentId#"/>,<cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.rating#"/>)
</cfquery>
An Important Tip
Although indirectly related to the rating widget tutorial, it’s worth mentioning this query is an example of two very important best practices that regularly are forgotten by CF developers:
- Always use the
<cfqueryparam>
tag when you’re creating dynamic SQL queries – otherwise you’re creating a barndoor-sized security gap and opening yourself up to SQL injection hacks.<cfqueryparam>
ensures at least the correct type of data and additionally creates a parameter binding, which usually executes faster than just using ColdFusion variables, as in...VALUES ('#arguments.contentId#')...
. - Remember to scope variables that are local to a function. Until ColdFusion 8 that had to be done by declaring function-local variables by using the
var
keyword, as in:<cfset var abc=345>
. ColdFusion 9 has introduced a new, alternative (and in my opinion, better) syntax by offering the local scope as shown above by naming the query objectlocal.qRating
.
Finishing Up
With that last step we have everything covered – nearly. When we were looking at the index.cfm
file earlier in the tutorial, we just saw a snippet of CFML code, rather than all of it. Because our page and the widget.cfm
use the Spry Rating class, we need to make sure we import those particular JavaScript and CSS files from the Spry library:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="https://www.w3.org/1999/xhtml">
<head>
...
<script language="JavaScript" type="text/javascript" src="js/spry/SpryRating.js"></script>
<link href="css/spry/SpryRating.css" rel="stylesheet" type="text/css" />
<style type="text/css">
.thankyou
{
float:left;
margin-top:1px;
}
</style>
</head>
There’s also an additional style rule for the thankyou
HTML class
to position the “Thanks for voting” output. The final output is shown below.
Thank-you message
The user has given the content 4 stars; a revote, as such, is impossible. Reloading the page provides updates to the score and amount of votes, and does enable this user to vote again. There are a variety of possible extensions of this widget to work around this issue and the feasibility of each depends on the use case of the widget. One could think of implementing a very simple, cookie-based solution, but also tie the widget into a proper authentication model that keeps track of user sessions and identifiers; that way, you could prohibit multiple voting on the same content by a given user.
So how would you rate your knowledge? Test yourself with our quiz and, If you’re one of the first 200, receive a printed copy of the Adobe ColdFusion Evangelism Kit. Don’t forget to download the ColdFusion 9 preview, the source code from this article, and have a go at building your first ColdFusion app.